Triggers on foreign tables
Hello.
Since the last discussion about it (/messages/by-id/CADyhKSUGP6oJb1pybTiMOP3s5fg_yOkgUTo-7RBcLTNVaJ57Pw@mail.gmail.com), I
finally managed to implement row triggers for foreign tables.
The attached patch does not contain regression tests or documentation yet, a
revised patch will include those sometime during the week. I'll add it to the
commitfest then.
A simple test scenario using postgres_fdw is however included as an
attachment.
The attached patch implements triggers for foreign tables according to the
design discussed in the link above.
For statement-level triggers, nothing has changed since the last patch.
Statement-level triggers are just allowed in the command checking.
For row-level triggers, it works as follows:
- during rewrite phase, every attribute of the foreign table is duplicated as
a resjunk entry if a trigger is defined on the relation. These entries are
then used to store the old values for a tuple. There is room for improvement
here: we could check if any trigger is in fact a row-level trigger
- during execution phase, the before triggers are fired exactly like triggers
on regular tables, except that old tuples are not fetched using their ctid,
but rebuilt using the previously-stored resjunk attributes.
- for after triggers, the whole queuing mechanism is bypassed for foreign
tables. This is IMO acceptable, since foreign tables cannot have constraints
or constraints triggers, and thus have not need for deferrable execution. This
design avoids the need for storing and retrieving/identifying remote tuples
until the query or transaction end.
- the duplicated resjunk attributes are identified by being:
- marked as resjunk in the targetlist
- not being system or whole-row attributes (varno > 0)
There is still one small issue with the attached patch: modifications to the
tuple performed by the foreign data wrapper (via the returned TupleTableSlot
in ExecForeignUpdate and ExecForeignInsert hooks) are not visible to the AFTER
trigger. This could be fixed by merging the planslot containing the resjunk
columns with the returned slot before calling the trigger, but I'm not really
sure how to safely perform that. Any advice ?
Many thanks to Kohei Kaigai for taking the time to help with the design.
--
Ronan Dunklau
http://dalibo.com - http://dalibo.org
Attachments:
foreign_triggers_v3.patchtext/x-patch; charset=UTF-8; name=foreign_triggers_v3.patchDownload
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b9cd88d..a509595 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3150,13 +3150,16 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
/* No command-specific prep needed */
break;
case AT_EnableTrig: /* ENABLE TRIGGER variants */
- case AT_EnableAlwaysTrig:
- case AT_EnableReplicaTrig:
case AT_EnableTrigAll:
case AT_EnableTrigUser:
case AT_DisableTrig: /* DISABLE TRIGGER variants */
case AT_DisableTrigAll:
case AT_DisableTrigUser:
+ ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE);
+ pass = AT_PASS_MISC;
+ break;
+ case AT_EnableReplicaTrig:
+ case AT_EnableAlwaysTrig:
case AT_EnableRule: /* ENABLE/DISABLE RULE variants */
case AT_EnableAlwaysRule:
case AT_EnableReplicaRule:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 4eff184..12870eb 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -75,6 +75,14 @@ static HeapTuple GetTupleForTrigger(EState *estate,
ItemPointer tid,
LockTupleMode lockmode,
TupleTableSlot **newSlot);
+
+static HeapTuple ExtractOldTuple(TupleTableSlot * mixedtupleslot,
+ ResultRelInfo * relinfo);
+static void ExecARForeignTrigger(EState * estate,
+ TriggerData LocTriggerData,
+ ResultRelInfo *relinfo,
+ int triggerEvent, int triggerType);
+
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
Trigger *trigger, TriggerEvent event,
Bitmapset *modifiedCols,
@@ -184,12 +192,22 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
RelationGetRelationName(rel)),
errdetail("Views cannot have TRUNCATE triggers.")));
}
+ else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ 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("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
-
+ }
if (!allowSystemTableMods && IsSystemRelation(rel))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1062,10 +1080,11 @@ RemoveTriggerById(Oid trigOid)
rel = heap_open(relid, AccessExclusiveLock);
if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_VIEW)
+ rel->rd_rel->relkind != RELKIND_VIEW &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view",
+ errmsg("\"%s\" is not a table, view or foreign table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
@@ -1166,10 +1185,11 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
form = (Form_pg_class) GETSTRUCT(tuple);
/* only tables and views can have triggers */
- if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW)
+ if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW &&
+ form->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view", rv->relname)));
+ errmsg("\"%s\" is not a table, view or foreign table", rv->relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
@@ -1844,9 +1864,7 @@ ExecCallTriggerFunc(TriggerData *trigdata,
*/
InitFunctionCallInfoData(fcinfo, finfo, 0,
InvalidOid, (Node *) trigdata, NULL);
-
pgstat_init_function_usage(&fcinfo, &fcusage);
-
MyTriggerDepth++;
PG_TRY();
{
@@ -1946,10 +1964,10 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo)
TupleTableSlot *
ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
- TupleTableSlot *slot)
+ TupleTableSlot *tupleslot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
- HeapTuple slottuple = ExecMaterializeSlot(slot);
+ HeapTuple slottuple = ExecMaterializeSlot(tupleslot);
HeapTuple newtuple = slottuple;
HeapTuple oldtuple;
TriggerData LocTriggerData;
@@ -2003,9 +2021,9 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if (newslot->tts_tupleDescriptor != tupdesc)
ExecSetSlotDescriptor(newslot, tupdesc);
ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
- slot = newslot;
+ tupleslot = newslot;
}
- return slot;
+ return tupleslot;
}
void
@@ -2015,8 +2033,26 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_row)
+ {
+ Relation rel = relinfo->ri_RelationDesc;
+
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE){
+ TriggerData LocTriggerData;
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ ExecARForeignTrigger(estate,
+ LocTriggerData,
+ relinfo,
+ TRIGGER_EVENT_INSERT,
+ TRIGGER_TYPE_INSERT);
+ }
+ else
+ {
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, trigtuple, recheckIndexes, NULL);
+ }
+ }
}
TupleTableSlot *
@@ -2146,7 +2182,8 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
bool
ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
bool result = true;
@@ -2155,9 +2192,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
HeapTuple newtuple;
TupleTableSlot *newSlot;
int i;
-
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- LockTupleExclusive, &newSlot);
+ Relation rel = relinfo->ri_RelationDesc;
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ LockTupleExclusive, &newSlot);
+ }
if (trigtuple == NULL)
return false;
@@ -2204,22 +2249,43 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
void
ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
-
if (trigdesc && trigdesc->trig_delete_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since there
+ * cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE){
+ TriggerData LocTriggerData;
+ trigtuple = ExtractOldTuple(tupleslot, relinfo);
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ ExecARForeignTrigger(estate,
+ LocTriggerData,
+ relinfo,
+ TRIGGER_EVENT_DELETE,
+ TRIGGER_TYPE_DELETE);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
NULL,
relinfo,
tupleid,
LockTupleExclusive,
NULL);
-
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
- true, trigtuple, NULL, NIL, NULL);
- heap_freetuple(trigtuple);
+ AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ true, trigtuple, NULL, NIL, NULL);
+ heap_freetuple(trigtuple);
+ }
}
}
@@ -2335,16 +2401,18 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TupleTableSlot *
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid, TupleTableSlot *slot)
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
- HeapTuple slottuple = ExecMaterializeSlot(slot);
+ HeapTuple slottuple = ExecMaterializeSlot(tupleslot);
HeapTuple newtuple = slottuple;
TriggerData LocTriggerData;
HeapTuple trigtuple;
HeapTuple oldtuple;
TupleTableSlot *newSlot;
int i;
+ Relation relation = relinfo->ri_RelationDesc;
Bitmapset *modifiedCols;
Bitmapset *keyCols;
LockTupleMode lockmode;
@@ -2355,7 +2423,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
* concurrency.
*/
modifiedCols = GetModifiedColumns(relinfo, estate);
- keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
+ keyCols = RelationGetIndexAttrBitmap(relation,
INDEX_ATTR_BITMAP_KEY);
if (bms_overlap(keyCols, modifiedCols))
lockmode = LockTupleExclusive;
@@ -2363,8 +2431,16 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
lockmode = LockTupleNoKeyExclusive;
/* get a copy of the on-disk tuple we are planning to update */
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- lockmode, &newSlot);
+ if (relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ lockmode, &newSlot);
+ }
if (trigtuple == NULL)
return NULL; /* cancel the update action */
@@ -2381,8 +2457,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
*/
if (newSlot != NULL)
{
- slot = ExecFilterJunk(relinfo->ri_junkFilter, newSlot);
- slottuple = ExecMaterializeSlot(slot);
+ tupleslot = ExecFilterJunk(relinfo->ri_junkFilter, newSlot);
+ slottuple = ExecMaterializeSlot(tupleslot);
newtuple = slottuple;
}
@@ -2439,31 +2515,54 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
if (newslot->tts_tupleDescriptor != tupdesc)
ExecSetSlotDescriptor(newslot, tupdesc);
ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
- slot = newslot;
+ tupleslot = newslot;
}
- return slot;
+ return tupleslot;
}
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid, HeapTuple newtuple,
+ ItemPointer tupleid,
+ HeapTuple newtuple,
+ TupleTableSlot *tupleslot,
List *recheckIndexes)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since there
+ * cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE){
+ TriggerData LocTriggerData;
+ trigtuple = ExtractOldTuple(tupleslot, relinfo);
+ LocTriggerData.tg_newtuple = newtuple;
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ ExecARForeignTrigger(estate,
+ LocTriggerData,
+ relinfo,
+ TRIGGER_EVENT_UPDATE,
+ TRIGGER_TYPE_UPDATE);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ true, trigtuple, newtuple, recheckIndexes,
+ GetModifiedColumns(relinfo, estate));
+ heap_freetuple(trigtuple);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
- true, trigtuple, newtuple, recheckIndexes,
- GetModifiedColumns(relinfo, estate));
- heap_freetuple(trigtuple);
+ }
}
}
@@ -2604,7 +2703,11 @@ GetTupleForTrigger(EState *estate,
HeapTupleData tuple;
HeapTuple result;
Buffer buffer;
-
+ /*
+ * Foreign tables tuples are NOT stored on the heap.
+ * Instead, old attributes values are stored as resjunk attributes in the
+ * original tuple. So, we build a new tuple from those attributes here.
+ */
if (newSlot != NULL)
{
HTSU_Result test;
@@ -2731,6 +2834,90 @@ ltrmark:;
}
/*
+ * Get an old tuple from a "mixed tuple", containing both the new values as well
+ * as well as the old ones as resjunk columns.
+ */
+static HeapTuple
+ExtractOldTuple(TupleTableSlot *planSlot,
+ ResultRelInfo *relinfo)
+{
+ Relation relation = relinfo->ri_RelationDesc;
+ TupleDesc base_tuple_desc = relation->rd_att;
+ Datum * datum = palloc0(sizeof(Datum) * base_tuple_desc->natts);
+ bool * isnull = palloc0(sizeof(bool) * base_tuple_desc->natts);
+ HeapTuple tuple;
+ /*
+ * NewSlot is not null: this indicates a BEFORE trigger.
+ */
+ JunkFilter * junk_filter = relinfo->ri_junkFilter;
+ List * target_list = junk_filter->jf_targetList;
+ ListCell * lc;
+ foreach(lc, target_list)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ /*
+ * Only consider previously added resjunk entries
+ */
+ if(tle->resjunk)
+ {
+ if(IsA(tle->expr, Var))
+ {
+ Var * junkvar = (Var * )tle->expr;
+ /*
+ * Do not copy system identifiers.
+ */
+ if(junkvar->varoattno > 0)
+ {
+ datum[junkvar->varoattno - 1] = slot_getattr(planSlot,
+ tle->resno,
+ &(isnull[junkvar->varoattno - 1]));
+ }
+ }
+ }
+ }
+ tuple = heap_form_tuple(base_tuple_desc, datum, isnull);
+ return tuple;
+}
+
+/*
+ * Fire a trigger for a trigger on a foreign table
+ */
+static void
+ExecARForeignTrigger(EState * estate,
+ TriggerData LocTriggerData,
+ ResultRelInfo *relinfo, int triggerEvent,
+ int triggerType)
+{
+ int i = 0;
+ Relation rel = relinfo->ri_RelationDesc;
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_relation = rel;
+ LocTriggerData.tg_event = triggerEvent |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_AFTER;
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[i];
+
+ if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
+ TRIGGER_TYPE_ROW,
+ TRIGGER_TYPE_AFTER,
+ triggerType))
+ continue;
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, LocTriggerData.tg_trigtuple, NULL))
+ continue;
+ LocTriggerData.tg_trigger = trigger;
+ ExecCallTriggerFunc(&LocTriggerData,
+ i,
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ }
+}
+
+/*
* Is trigger enabled to fire?
*/
static bool
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 189df71..c5a25d2 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -342,7 +342,8 @@ ExecDelete(ItemPointer tupleid,
bool dodelete;
dodelete = ExecBRDeleteTriggers(estate, epqstate, resultRelInfo,
- tupleid);
+ tupleid,
+ epqstate->origslot);
if (!dodelete) /* "do nothing" */
return NULL;
@@ -488,7 +489,7 @@ ldelete:;
(estate->es_processed)++;
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, planSlot);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
@@ -609,7 +610,8 @@ ExecUpdate(ItemPointer tupleid,
resultRelInfo->ri_TrigDesc->trig_update_before_row)
{
slot = ExecBRUpdateTriggers(estate, epqstate, resultRelInfo,
- tupleid, slot);
+ tupleid,
+ slot);
if (slot == NULL) /* "do nothing" */
return NULL;
@@ -788,7 +790,7 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
+ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, planSlot,
recheckIndexes);
list_free(recheckIndexes);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 50cb753..deb1fae 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1199,13 +1199,43 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
* Let the foreign table's FDW add whatever junk TLEs it wants.
*/
FdwRoutine *fdwroutine;
+ TupleDesc desc = target_relation->rd_att;
+ int i;
fdwroutine = GetFdwRoutineForRelation(target_relation, false);
if (fdwroutine->AddForeignUpdateTargets != NULL)
fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
target_relation);
-
+ /*
+ * If there is any row-level trigger on the table, add junk attributes
+ * for the old ones.
+ */
+ if (target_relation->trigdesc != NULL)
+ {
+ for (i = 0; i < desc->natts; i++)
+ {
+ Form_pg_attribute att = desc->attrs[i];
+ if(!att->attisdropped)
+ {
+ TargetEntry *tle;
+ Var * var = makeVar(parsetree->resultRelation,
+ att->attnum,
+ att->atttypid,
+ att->atttypmod,
+ att->attcollation,
+ 0);
+ /*
+ * Identify those newly created vars with a varoattno at -1
+ */
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->targetList) + 1,
+ strdup(NameStr(att->attname)),
+ true);
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+ }
+ }
+ }
return;
}
else
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 411a66d..079bbac 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -147,10 +147,12 @@ extern void ExecASDeleteTriggers(EState *estate,
extern bool ExecBRDeleteTriggers(EState *estate,
EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot);
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -162,11 +164,12 @@ extern TupleTableSlot *ExecBRUpdateTriggers(EState *estate,
EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
- TupleTableSlot *slot);
+ TupleTableSlot *tupleslot);
extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple newtuple,
+ TupleTableSlot *tupleslot,
List *recheckIndexes);
extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
Le mardi 7 janvier 2014 12:11:28 Ronan Dunklau a écrit :
The attached patch does not contain regression tests or documentation yet, a
revised patch will include those sometime during the week. I'll add it to
the commitfest then.
Please find attached a new patch which fixes some issues with WHEN conditions
and provide a set of regression tests as well as the documentation.
The previously mentioned issue regarding values modified by the FDW is however
not fixed.
--
Ronan Dunklau
http://dalibo.com - http://dalibo.org
Attachments:
foreign_triggers_v4.patchtext/x-patch; charset=UTF-8; name=foreign_triggers_v4.patchDownload
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 38c6cf8..5982eb5 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2372,3 +2372,427 @@ select * from rem1;
11 | bye remote
(4 rows)
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON ft1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON ft1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+
+ argstr text;
+ relid text;
+
+begin
+
+ relid := TG_relid::regclass;
+
+ -- plpgsql can't discover its trigger data in a hash like perl and python
+ -- can, or by a sort of reflection like tcl can,
+ -- so we have to hard code the names.
+ raise NOTICE 'TG_NAME: %', TG_name;
+ raise NOTICE 'TG_WHEN: %', TG_when;
+ raise NOTICE 'TG_LEVEL: %', TG_level;
+ raise NOTICE 'TG_OP: %', TG_op;
+ raise NOTICE 'TG_RELID::regclass: %', relid;
+ raise NOTICE 'TG_RELNAME: %', TG_relname;
+ raise NOTICE 'TG_TABLE_NAME: %', TG_table_name;
+ raise NOTICE 'TG_TABLE_SCHEMA: %', TG_table_schema;
+ raise NOTICE 'TG_NARGS: %', TG_nargs;
+
+ argstr := '[';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+ argstr := argstr || ']';
+ raise NOTICE 'TG_ARGV: %', argstr;
+
+ if TG_OP != 'INSERT' then
+ raise NOTICE 'OLD: %', OLD;
+ end if;
+
+ if TG_OP != 'DELETE' then
+ raise NOTICE 'NEW: %', NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+
+end;
+$$;
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+delete from rem1;
+NOTICE: TG_NAME: trig_row_before
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (1,hi)
+NOTICE: TG_NAME: trig_row_after
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (1,hi)
+NOTICE: TG_NAME: trig_row_before
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (10,"hi remote")
+NOTICE: TG_NAME: trig_row_after
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (10,"hi remote")
+NOTICE: TG_NAME: trig_row_before
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (2,bye)
+NOTICE: TG_NAME: trig_row_after
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (2,bye)
+NOTICE: TG_NAME: trig_row_before
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (11,"bye remote")
+NOTICE: TG_NAME: trig_row_after
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (11,"bye remote")
+insert into rem1 values(1,'insert');
+NOTICE: TG_NAME: trig_row_before
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: INSERT
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: NEW: (1,insert)
+NOTICE: TG_NAME: trig_row_after
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: INSERT
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: NEW: (1,insert)
+update rem1 set f2 = 'update' where f1 = 1;
+NOTICE: TG_NAME: trig_row_before
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: UPDATE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (1,insert)
+NOTICE: NEW: (1,update)
+NOTICE: TG_NAME: trig_row_after
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: UPDATE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (1,insert)
+NOTICE: NEW: (1,update)
+update rem1 set f2 = f2 || f2;
+NOTICE: TG_NAME: trig_row_before
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: UPDATE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (1,update)
+NOTICE: NEW: (1,updateupdate)
+NOTICE: TG_NAME: trig_row_after
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: UPDATE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (1,update)
+NOTICE: NEW: (1,updateupdate)
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DELETE from rem1;
+-- Test WHEN conditions
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+NOTICE: TG_NAME: trig_row_before_insupd
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: INSERT
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: NEW: (2,update)
+NOTICE: TG_NAME: trig_row_after_insupd
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: INSERT
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: NEW: (2,update)
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+NOTICE: TG_NAME: trig_row_before_insupd
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: UPDATE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (2,update)
+NOTICE: NEW: (2,"update update")
+NOTICE: TG_NAME: trig_row_after_insupd
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: UPDATE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (2,update)
+NOTICE: NEW: (2,"update update")
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+NOTICE: TG_NAME: trig_row_before_delete
+NOTICE: TG_WHEN: BEFORE
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (2,"update update")
+NOTICE: TG_NAME: trig_row_after_delete
+NOTICE: TG_WHEN: AFTER
+NOTICE: TG_LEVEL: ROW
+NOTICE: TG_OP: DELETE
+NOTICE: TG_RELID::regclass: rem1
+NOTICE: TG_RELNAME: rem1
+NOTICE: TG_TABLE_NAME: rem1
+NOTICE: TG_TABLE_SCHEMA: public
+NOTICE: TG_NARGS: 2
+NOTICE: TG_ARGV: [23, skidoo]
+NOTICE: OLD: (2,"update update")
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+-- Test various RETURN statements in BEFORE triggers.
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------
+ insert triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+ 2 | insert triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------
+ 1 | triggered !
+ 2 | triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------
+ skidoo triggered !
+ skidoo triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | skidoo triggered !
+ 2 | skidoo triggered !
+(2 rows)
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DELETE from rem1;
+INSERT INTO rem1 VALUES (1, 'test');
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+UPDATE rem1 SET f2 = 'test2';
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DELETE from rem1;
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index ce8bb75..db58014 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -384,3 +384,188 @@ insert into loc1(f2) values('bye');
insert into rem1(f2) values('bye remote');
select * from loc1;
select * from rem1;
+
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON ft1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON ft1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+
+ argstr text;
+ relid text;
+
+begin
+
+ relid := TG_relid::regclass;
+
+ -- plpgsql can't discover its trigger data in a hash like perl and python
+ -- can, or by a sort of reflection like tcl can,
+ -- so we have to hard code the names.
+ raise NOTICE 'TG_NAME: %', TG_name;
+ raise NOTICE 'TG_WHEN: %', TG_when;
+ raise NOTICE 'TG_LEVEL: %', TG_level;
+ raise NOTICE 'TG_OP: %', TG_op;
+ raise NOTICE 'TG_RELID::regclass: %', relid;
+ raise NOTICE 'TG_RELNAME: %', TG_relname;
+ raise NOTICE 'TG_TABLE_NAME: %', TG_table_name;
+ raise NOTICE 'TG_TABLE_SCHEMA: %', TG_table_schema;
+ raise NOTICE 'TG_NARGS: %', TG_nargs;
+
+ argstr := '[';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+ argstr := argstr || ']';
+ raise NOTICE 'TG_ARGV: %', argstr;
+
+ if TG_OP != 'INSERT' then
+ raise NOTICE 'OLD: %', OLD;
+ end if;
+
+ if TG_OP != 'DELETE' then
+ raise NOTICE 'NEW: %', NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+
+end;
+$$;
+
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+delete from rem1;
+insert into rem1 values(1,'insert');
+update rem1 set f2 = 'update' where f1 = 1;
+update rem1 set f2 = f2 || f2;
+
+
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+
+DELETE from rem1;
+
+
+-- Test WHEN conditions
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+
+
+-- Test various RETURN statements in BEFORE triggers.
+
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+
+DELETE from rem1;
+
+INSERT INTO rem1 VALUES (1, 'test');
+
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+
+SELECT * from loc1;
+
+UPDATE rem1 SET f2 = 'test2';
+
+SELECT * from loc1;
+
+DELETE from rem1;
+
+SELECT * from loc1;
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index e5ec738..07bc757 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -93,7 +93,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<para>
The following table summarizes which types of triggers may be used on
- tables and views:
+ tables, views and foreign tables:
</para>
<informaltable id="supported-trigger-types">
@@ -110,8 +110,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>BEFORE</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -121,8 +121,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>AFTER</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -244,7 +244,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
<term><replaceable class="parameter">table_name</replaceable></term>
<listitem>
<para>
- The name (optionally schema-qualified) of the table or view the trigger
+ The name (optionally schema-qualified) of the table, view or foreign table the trigger
is for.
</para>
</listitem>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index f579340..6d9d3ff 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -33,11 +33,11 @@
<para>
A trigger is a specification that the database should automatically
execute a particular function whenever a certain type of operation is
- performed. Triggers can be attached to both tables and views.
+ performed. Triggers can be attached to tables, views and foreign tables.
</para>
<para>
- On tables, triggers can be defined to execute either before or after any
+ On tables and foreign tables, triggers can be defined to execute either before or after any
<command>INSERT</command>, <command>UPDATE</command>, or
<command>DELETE</command> operation, either once per modified row,
or once per <acronym>SQL</acronym> statement.
@@ -46,7 +46,7 @@
<command>UPDATE</command> statement.
Triggers can also fire for <command>TRUNCATE</command> statements.
If a trigger event occurs, the trigger's function is called at the
- appropriate time to handle the event.
+ appropriate time to handle the event. Foreign tables do not support the TRUNCATE statement at all.
</para>
<para>
@@ -96,6 +96,7 @@
after may only be defined at statement level, while triggers that fire
instead of an <command>INSERT</command>, <command>UPDATE</command>,
or <command>DELETE</command> may only be defined at row level.
+ On foreign tables, triggers can not be defined at row level.
</para>
<para>
@@ -108,11 +109,13 @@
statement starts to do anything, while statement-level <literal>AFTER</>
triggers fire at the very end of the statement. These types of
triggers may be defined on tables or views. Row-level <literal>BEFORE</>
- triggers fire immediately before a particular row is operated on,
- while row-level <literal>AFTER</> triggers fire at the end of the
+ triggers fire immediately before a particular row is operated on.
+ For regular tables, row-level <literal>AFTER</> triggers fire at the end of the
statement (but before any statement-level <literal>AFTER</> triggers).
- These types of triggers may only be defined on tables. Row-level
- <literal>INSTEAD OF</> triggers may only be defined on views, and fire
+ For foreing tables, row-level <literal>AFTER</> triggers fire immediately
+ after a particular row is operated on.
+ These types of triggers may only be defined on tables and foreign tables.
+ Row-level <literal>INSTEAD OF</> triggers may only be defined on views, and fire
immediately as each row in the view is identified as needing to be
operated on.
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b9cd88d..a509595 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3150,13 +3150,16 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
/* No command-specific prep needed */
break;
case AT_EnableTrig: /* ENABLE TRIGGER variants */
- case AT_EnableAlwaysTrig:
- case AT_EnableReplicaTrig:
case AT_EnableTrigAll:
case AT_EnableTrigUser:
case AT_DisableTrig: /* DISABLE TRIGGER variants */
case AT_DisableTrigAll:
case AT_DisableTrigUser:
+ ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE);
+ pass = AT_PASS_MISC;
+ break;
+ case AT_EnableReplicaTrig:
+ case AT_EnableAlwaysTrig:
case AT_EnableRule: /* ENABLE/DISABLE RULE variants */
case AT_EnableAlwaysRule:
case AT_EnableReplicaRule:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 4eff184..da3c568 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -75,6 +75,14 @@ static HeapTuple GetTupleForTrigger(EState *estate,
ItemPointer tid,
LockTupleMode lockmode,
TupleTableSlot **newSlot);
+
+static HeapTuple ExtractOldTuple(TupleTableSlot * mixedtupleslot,
+ ResultRelInfo * relinfo);
+static void ExecARForeignTrigger(EState * estate,
+ TriggerData LocTriggerData,
+ ResultRelInfo *relinfo,
+ int triggerEvent, int triggerType);
+
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
Trigger *trigger, TriggerEvent event,
Bitmapset *modifiedCols,
@@ -184,12 +192,22 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
RelationGetRelationName(rel)),
errdetail("Views cannot have TRUNCATE triggers.")));
}
+ else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ 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("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
-
+ }
if (!allowSystemTableMods && IsSystemRelation(rel))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1062,10 +1080,11 @@ RemoveTriggerById(Oid trigOid)
rel = heap_open(relid, AccessExclusiveLock);
if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_VIEW)
+ rel->rd_rel->relkind != RELKIND_VIEW &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view",
+ errmsg("\"%s\" is not a table, view or foreign table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
@@ -1166,10 +1185,11 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
form = (Form_pg_class) GETSTRUCT(tuple);
/* only tables and views can have triggers */
- if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW)
+ if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW &&
+ form->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view", rv->relname)));
+ errmsg("\"%s\" is not a table, view or foreign table", rv->relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
@@ -1844,9 +1864,7 @@ ExecCallTriggerFunc(TriggerData *trigdata,
*/
InitFunctionCallInfoData(fcinfo, finfo, 0,
InvalidOid, (Node *) trigdata, NULL);
-
pgstat_init_function_usage(&fcinfo, &fcusage);
-
MyTriggerDepth++;
PG_TRY();
{
@@ -1946,10 +1964,10 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo)
TupleTableSlot *
ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
- TupleTableSlot *slot)
+ TupleTableSlot *tupleslot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
- HeapTuple slottuple = ExecMaterializeSlot(slot);
+ HeapTuple slottuple = ExecMaterializeSlot(tupleslot);
HeapTuple newtuple = slottuple;
HeapTuple oldtuple;
TriggerData LocTriggerData;
@@ -2003,9 +2021,9 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if (newslot->tts_tupleDescriptor != tupdesc)
ExecSetSlotDescriptor(newslot, tupdesc);
ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
- slot = newslot;
+ tupleslot = newslot;
}
- return slot;
+ return tupleslot;
}
void
@@ -2015,8 +2033,27 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_row)
+ {
+ Relation rel = relinfo->ri_RelationDesc;
+
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE){
+ TriggerData LocTriggerData;
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_newtuple = trigtuple;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ ExecARForeignTrigger(estate,
+ LocTriggerData,
+ relinfo,
+ TRIGGER_EVENT_INSERT,
+ TRIGGER_TYPE_INSERT);
+ }
+ else
+ {
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, trigtuple, recheckIndexes, NULL);
+ }
+ }
}
TupleTableSlot *
@@ -2146,7 +2183,8 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
bool
ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
bool result = true;
@@ -2155,9 +2193,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
HeapTuple newtuple;
TupleTableSlot *newSlot;
int i;
-
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- LockTupleExclusive, &newSlot);
+ Relation rel = relinfo->ri_RelationDesc;
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ LockTupleExclusive, &newSlot);
+ }
if (trigtuple == NULL)
return false;
@@ -2204,22 +2250,43 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
void
ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
-
if (trigdesc && trigdesc->trig_delete_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since there
+ * cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE){
+ TriggerData LocTriggerData;
+ trigtuple = ExtractOldTuple(tupleslot, relinfo);
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ ExecARForeignTrigger(estate,
+ LocTriggerData,
+ relinfo,
+ TRIGGER_EVENT_DELETE,
+ TRIGGER_TYPE_DELETE);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
NULL,
relinfo,
tupleid,
LockTupleExclusive,
NULL);
-
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
- true, trigtuple, NULL, NIL, NULL);
- heap_freetuple(trigtuple);
+ AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ true, trigtuple, NULL, NIL, NULL);
+ heap_freetuple(trigtuple);
+ }
}
}
@@ -2335,16 +2402,18 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TupleTableSlot *
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid, TupleTableSlot *slot)
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
- HeapTuple slottuple = ExecMaterializeSlot(slot);
+ HeapTuple slottuple = ExecMaterializeSlot(tupleslot);
HeapTuple newtuple = slottuple;
TriggerData LocTriggerData;
HeapTuple trigtuple;
HeapTuple oldtuple;
TupleTableSlot *newSlot;
int i;
+ Relation relation = relinfo->ri_RelationDesc;
Bitmapset *modifiedCols;
Bitmapset *keyCols;
LockTupleMode lockmode;
@@ -2355,7 +2424,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
* concurrency.
*/
modifiedCols = GetModifiedColumns(relinfo, estate);
- keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
+ keyCols = RelationGetIndexAttrBitmap(relation,
INDEX_ATTR_BITMAP_KEY);
if (bms_overlap(keyCols, modifiedCols))
lockmode = LockTupleExclusive;
@@ -2363,8 +2432,16 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
lockmode = LockTupleNoKeyExclusive;
/* get a copy of the on-disk tuple we are planning to update */
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- lockmode, &newSlot);
+ if (relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ lockmode, &newSlot);
+ }
if (trigtuple == NULL)
return NULL; /* cancel the update action */
@@ -2381,8 +2458,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
*/
if (newSlot != NULL)
{
- slot = ExecFilterJunk(relinfo->ri_junkFilter, newSlot);
- slottuple = ExecMaterializeSlot(slot);
+ tupleslot = ExecFilterJunk(relinfo->ri_junkFilter, newSlot);
+ slottuple = ExecMaterializeSlot(tupleslot);
newtuple = slottuple;
}
@@ -2439,31 +2516,54 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
if (newslot->tts_tupleDescriptor != tupdesc)
ExecSetSlotDescriptor(newslot, tupdesc);
ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
- slot = newslot;
+ tupleslot = newslot;
}
- return slot;
+ return tupleslot;
}
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid, HeapTuple newtuple,
+ ItemPointer tupleid,
+ HeapTuple newtuple,
+ TupleTableSlot *tupleslot,
List *recheckIndexes)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since there
+ * cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE){
+ TriggerData LocTriggerData;
+ trigtuple = ExtractOldTuple(tupleslot, relinfo);
+ LocTriggerData.tg_newtuple = newtuple;
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ ExecARForeignTrigger(estate,
+ LocTriggerData,
+ relinfo,
+ TRIGGER_EVENT_UPDATE,
+ TRIGGER_TYPE_UPDATE);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ true, trigtuple, newtuple, recheckIndexes,
+ GetModifiedColumns(relinfo, estate));
+ heap_freetuple(trigtuple);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
- true, trigtuple, newtuple, recheckIndexes,
- GetModifiedColumns(relinfo, estate));
- heap_freetuple(trigtuple);
+ }
}
}
@@ -2604,7 +2704,11 @@ GetTupleForTrigger(EState *estate,
HeapTupleData tuple;
HeapTuple result;
Buffer buffer;
-
+ /*
+ * Foreign tables tuples are NOT stored on the heap.
+ * Instead, old attributes values are stored as resjunk attributes in the
+ * original tuple. So, we build a new tuple from those attributes here.
+ */
if (newSlot != NULL)
{
HTSU_Result test;
@@ -2731,6 +2835,90 @@ ltrmark:;
}
/*
+ * Get an old tuple from a "mixed tuple", containing both the new values as well
+ * as well as the old ones as resjunk columns.
+ */
+static HeapTuple
+ExtractOldTuple(TupleTableSlot *planSlot,
+ ResultRelInfo *relinfo)
+{
+ Relation relation = relinfo->ri_RelationDesc;
+ TupleDesc base_tuple_desc = relation->rd_att;
+ Datum * datum = palloc0(sizeof(Datum) * base_tuple_desc->natts);
+ bool * isnull = palloc0(sizeof(bool) * base_tuple_desc->natts);
+ HeapTuple tuple;
+ /*
+ * NewSlot is not null: this indicates a BEFORE trigger.
+ */
+ JunkFilter * junk_filter = relinfo->ri_junkFilter;
+ List * target_list = junk_filter->jf_targetList;
+ ListCell * lc;
+ foreach(lc, target_list)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ /*
+ * Only consider previously added resjunk entries
+ */
+ if(tle->resjunk)
+ {
+ if(IsA(tle->expr, Var))
+ {
+ Var * junkvar = (Var * )tle->expr;
+ /*
+ * Do not copy system identifiers.
+ */
+ if(junkvar->varoattno > 0)
+ {
+ datum[junkvar->varoattno - 1] = slot_getattr(planSlot,
+ tle->resno,
+ &(isnull[junkvar->varoattno - 1]));
+ }
+ }
+ }
+ }
+ tuple = heap_form_tuple(base_tuple_desc, datum, isnull);
+ return tuple;
+}
+
+/*
+ * Fire a trigger for a trigger on a foreign table
+ */
+static void
+ExecARForeignTrigger(EState * estate,
+ TriggerData LocTriggerData,
+ ResultRelInfo *relinfo, int triggerEvent,
+ int triggerType)
+{
+ int i = 0;
+ Relation rel = relinfo->ri_RelationDesc;
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_relation = rel;
+ LocTriggerData.tg_event = triggerEvent |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_AFTER;
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[i];
+
+ if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
+ TRIGGER_TYPE_ROW,
+ TRIGGER_TYPE_AFTER,
+ triggerType))
+ continue;
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, LocTriggerData.tg_trigtuple, LocTriggerData.tg_newtuple))
+ continue;
+ LocTriggerData.tg_trigger = trigger;
+ ExecCallTriggerFunc(&LocTriggerData,
+ i,
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ }
+}
+
+/*
* Is trigger enabled to fire?
*/
static bool
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 189df71..c5a25d2 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -342,7 +342,8 @@ ExecDelete(ItemPointer tupleid,
bool dodelete;
dodelete = ExecBRDeleteTriggers(estate, epqstate, resultRelInfo,
- tupleid);
+ tupleid,
+ epqstate->origslot);
if (!dodelete) /* "do nothing" */
return NULL;
@@ -488,7 +489,7 @@ ldelete:;
(estate->es_processed)++;
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, planSlot);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
@@ -609,7 +610,8 @@ ExecUpdate(ItemPointer tupleid,
resultRelInfo->ri_TrigDesc->trig_update_before_row)
{
slot = ExecBRUpdateTriggers(estate, epqstate, resultRelInfo,
- tupleid, slot);
+ tupleid,
+ slot);
if (slot == NULL) /* "do nothing" */
return NULL;
@@ -788,7 +790,7 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
+ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, planSlot,
recheckIndexes);
list_free(recheckIndexes);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 50cb753..deb1fae 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1199,13 +1199,43 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
* Let the foreign table's FDW add whatever junk TLEs it wants.
*/
FdwRoutine *fdwroutine;
+ TupleDesc desc = target_relation->rd_att;
+ int i;
fdwroutine = GetFdwRoutineForRelation(target_relation, false);
if (fdwroutine->AddForeignUpdateTargets != NULL)
fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
target_relation);
-
+ /*
+ * If there is any row-level trigger on the table, add junk attributes
+ * for the old ones.
+ */
+ if (target_relation->trigdesc != NULL)
+ {
+ for (i = 0; i < desc->natts; i++)
+ {
+ Form_pg_attribute att = desc->attrs[i];
+ if(!att->attisdropped)
+ {
+ TargetEntry *tle;
+ Var * var = makeVar(parsetree->resultRelation,
+ att->attnum,
+ att->atttypid,
+ att->atttypmod,
+ att->attcollation,
+ 0);
+ /*
+ * Identify those newly created vars with a varoattno at -1
+ */
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->targetList) + 1,
+ strdup(NameStr(att->attname)),
+ true);
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+ }
+ }
+ }
return;
}
else
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 411a66d..079bbac 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -147,10 +147,12 @@ extern void ExecASDeleteTriggers(EState *estate,
extern bool ExecBRDeleteTriggers(EState *estate,
EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot);
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ TupleTableSlot *tupleslot);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -162,11 +164,12 @@ extern TupleTableSlot *ExecBRUpdateTriggers(EState *estate,
EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
- TupleTableSlot *slot);
+ TupleTableSlot *tupleslot);
extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple newtuple,
+ TupleTableSlot *tupleslot,
List *recheckIndexes);
extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 60506e0..3405b6c 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -1158,6 +1158,36 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
ERROR: must be owner of foreign server s9
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+ERROR: "foreign_table_1" is a foreign table
+DETAIL: Foreign Tables cannot have constraint triggers.
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+DROP FUNCTION dummy_trigger();
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
ERROR: foreign table "no_table" does not exist
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index f819eb1..6166097 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -470,6 +470,43 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+
+DROP FUNCTION dummy_trigger();
+
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
DROP FOREIGN TABLE IF EXISTS no_table;
On Tue, Jan 07, 2014 at 12:11:28PM +0100, Ronan Dunklau wrote:
Since the last discussion about it (/messages/by-id/CADyhKSUGP6oJb1pybTiMOP3s5fg_yOkgUTo-7RBcLTNVaJ57Pw@mail.gmail.com), I
finally managed to implement row triggers for foreign tables.
For statement-level triggers, nothing has changed since the last patch.
Statement-level triggers are just allowed in the command checking.For row-level triggers, it works as follows:
- during rewrite phase, every attribute of the foreign table is duplicated as
a resjunk entry if a trigger is defined on the relation. These entries are
then used to store the old values for a tuple. There is room for improvement
here: we could check if any trigger is in fact a row-level trigger
The need here is awfully similar to a need of INSTEAD OF triggers on views.
For them, we add a single "wholerow" resjunk TLE. Is there a good reason to
do it differently for triggers on foreign tables?
- for after triggers, the whole queuing mechanism is bypassed for foreign
tables. This is IMO acceptable, since foreign tables cannot have constraints
or constraints triggers, and thus have not need for deferrable execution. This
design avoids the need for storing and retrieving/identifying remote tuples
until the query or transaction end.
Whether an AFTER ROW trigger is deferred determines whether it runs at the end
of the firing query or at the end of the firing query's transaction. In all
cases, every BEFORE ROW trigger of a given query fires before any AFTER ROW
trigger of the same query. SQL requires that. This proposal would give
foreign table AFTER ROW triggers a novel firing time; let's not do that.
I think the options going forward are either (a) design a way to queue foreign
table AFTER ROW triggers such that we can get the old and/or new rows at the
end of the query or (b) not support AFTER ROW triggers on foreign tables for
the time being.
It's not clear to me whether SQL/MED contemplates triggers on foreign tables.
Its <drop basic column definition> General Rules do mention the possibility of
a reference from a trigger column list. On the other hand, I see nothing
overriding the fact that CREATE TRIGGER only targets base tables. Is this
clearer to anyone else? (This is a minor point, mainly bearing on the
Compatibility section of the CREATE TRIGGER documentation.)
- the duplicated resjunk attributes are identified by being:
- marked as resjunk in the targetlist
- not being system or whole-row attributes (varno > 0)There is still one small issue with the attached patch: modifications to the
tuple performed by the foreign data wrapper (via the returned TupleTableSlot
in ExecForeignUpdate and ExecForeignInsert hooks) are not visible to the AFTER
trigger. This could be fixed by merging the planslot containing the resjunk
columns with the returned slot before calling the trigger, but I'm not really
sure how to safely perform that. Any advice ?
Currently, FDWs are permitted to skip returning columns not actually
referenced by any RETURNING clause. I would change that part of the API
contract to require returning all columns when an AFTER ROW trigger is
involved. You can't get around doing that by merging old column values,
because, among other reasons, an INSERT does not have those values at all.
On Tue, Jan 07, 2014 at 05:31:10PM +0100, Ronan Dunklau wrote:
--- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out
+NOTICE: TG_NAME: trig_row_after +NOTICE: TG_WHEN: AFTER +NOTICE: TG_LEVEL: ROW +NOTICE: TG_OP: DELETE +NOTICE: TG_RELID::regclass: rem1 +NOTICE: TG_RELNAME: rem1 +NOTICE: TG_TABLE_NAME: rem1 +NOTICE: TG_TABLE_SCHEMA: public +NOTICE: TG_NARGS: 2 +NOTICE: TG_ARGV: [23, skidoo] +NOTICE: OLD: (2,bye) +NOTICE: TG_NAME: trig_row_before +NOTICE: TG_WHEN: BEFORE +NOTICE: TG_LEVEL: ROW +NOTICE: TG_OP: DELETE +NOTICE: TG_RELID::regclass: rem1 +NOTICE: TG_RELNAME: rem1 +NOTICE: TG_TABLE_NAME: rem1 +NOTICE: TG_TABLE_SCHEMA: public +NOTICE: TG_NARGS: 2 +NOTICE: TG_ARGV: [23, skidoo] +NOTICE: OLD: (11,"bye remote") +NOTICE: TG_NAME: trig_row_after +NOTICE: TG_WHEN: AFTER +NOTICE: TG_LEVEL: ROW +NOTICE: TG_OP: DELETE +NOTICE: TG_RELID::regclass: rem1 +NOTICE: TG_RELNAME: rem1 +NOTICE: TG_TABLE_NAME: rem1 +NOTICE: TG_TABLE_SCHEMA: public +NOTICE: TG_NARGS: 2 +NOTICE: TG_ARGV: [23, skidoo] +NOTICE: OLD: (11,"bye remote") +insert into rem1 values(1,'insert');
Would you trim the verbosity a bit? Maybe merge several of the TG_ fields
onto one line, and remove the low-importance ones. Perhaps issue one line
like this in place of all the current TG_ lines:
NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -384,3 +384,188 @@ insert into loc1(f2) values('bye'); insert into rem1(f2) values('bye remote'); select * from loc1; select * from rem1; + +-- ==================================================================+-- test local triggers +-- ==================================================================+ +-- Trigger functions "borrowed" from triggers regress test. +CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; + RETURN NULL; +END;$$; + +CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON ft1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON ft1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
No test actually fires these triggers.
--- a/doc/src/sgml/trigger.sgml +++ b/doc/src/sgml/trigger.sgml
@@ -96,6 +96,7 @@ after may only be defined at statement level, while triggers that fire instead of an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command> may only be defined at row level. + On foreign tables, triggers can not be defined at row level.
This is obsolete.
--- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -3150,13 +3150,16 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, /* No command-specific prep needed */ break; case AT_EnableTrig: /* ENABLE TRIGGER variants */ - case AT_EnableAlwaysTrig: - case AT_EnableReplicaTrig: case AT_EnableTrigAll: case AT_EnableTrigUser: case AT_DisableTrig: /* DISABLE TRIGGER variants */ case AT_DisableTrigAll: case AT_DisableTrigUser: + ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE); + pass = AT_PASS_MISC; + break; + case AT_EnableReplicaTrig: + case AT_EnableAlwaysTrig:
Why permit some variants, but not every variant, of ALTER TABLE t ENABLE
TRIGGER <type> on foreign tables?
case AT_EnableRule: /* ENABLE/DISABLE RULE variants */ case AT_EnableAlwaysRule: case AT_EnableReplicaRule: diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 4eff184..da3c568 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c
@@ -1844,9 +1864,7 @@ ExecCallTriggerFunc(TriggerData *trigdata,
*/
InitFunctionCallInfoData(fcinfo, finfo, 0,
InvalidOid, (Node *) trigdata, NULL);
-
pgstat_init_function_usage(&fcinfo, &fcusage);
-
Please don't change unrelated whitespace.
MyTriggerDepth++;
PG_TRY();
{
@@ -1946,10 +1964,10 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo)TupleTableSlot * ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, - TupleTableSlot *slot) + TupleTableSlot *tupleslot) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; - HeapTuple slottuple = ExecMaterializeSlot(slot); + HeapTuple slottuple = ExecMaterializeSlot(tupleslot); HeapTuple newtuple = slottuple; HeapTuple oldtuple; TriggerData LocTriggerData; @@ -2003,9 +2021,9 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, if (newslot->tts_tupleDescriptor != tupdesc) ExecSetSlotDescriptor(newslot, tupdesc); ExecStoreTuple(newtuple, newslot, InvalidBuffer, false); - slot = newslot; + tupleslot = newslot; } - return slot; + return tupleslot;
Keep the old variable name. Exceptions can be made if the name was deceptive,
but "tupleslot" communicates the same thing as "slot".
@@ -2146,7 +2183,8 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo) bool ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, - ItemPointer tupleid) + ItemPointer tupleid, + TupleTableSlot *tupleslot)
The new argument is unused.
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE){ + TriggerData LocTriggerData; + trigtuple = ExtractOldTuple(tupleslot, relinfo); + LocTriggerData.tg_trigtuple = trigtuple; + LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_trigtuplebuf = InvalidBuffer; + LocTriggerData.tg_newtuplebuf = InvalidBuffer; + ExecARForeignTrigger(estate, + LocTriggerData, + relinfo, + TRIGGER_EVENT_DELETE, + TRIGGER_TYPE_DELETE); + } + else + { + trigtuple = GetTupleForTrigger(estate,
Please use pgindent to fix the formatting of your new code. It's fine to
introduce occasional whitespace errors, but they're unusually-plentiful here.
@@ -2604,7 +2704,11 @@ GetTupleForTrigger(EState *estate, HeapTupleData tuple; HeapTuple result; Buffer buffer; - + /* + * Foreign tables tuples are NOT stored on the heap. + * Instead, old attributes values are stored as resjunk attributes in the + * original tuple. So, we build a new tuple from those attributes here. + */
Obsolete comment. That's done elsewhere, not here.
@@ -2731,6 +2835,90 @@ ltrmark:;
}/* + * Get an old tuple from a "mixed tuple", containing both the new values as well + * as well as the old ones as resjunk columns. + */ +static HeapTuple +ExtractOldTuple(TupleTableSlot *planSlot, + ResultRelInfo *relinfo) +{ + Relation relation = relinfo->ri_RelationDesc; + TupleDesc base_tuple_desc = relation->rd_att; + Datum * datum = palloc0(sizeof(Datum) * base_tuple_desc->natts); + bool * isnull = palloc0(sizeof(bool) * base_tuple_desc->natts); + HeapTuple tuple; + /* + * NewSlot is not null: this indicates a BEFORE trigger. + */ + JunkFilter * junk_filter = relinfo->ri_junkFilter; + List * target_list = junk_filter->jf_targetList; + ListCell * lc; + foreach(lc, target_list) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + /* + * Only consider previously added resjunk entries + */ + if(tle->resjunk) + { + if(IsA(tle->expr, Var)) + { + Var * junkvar = (Var * )tle->expr; + /* + * Do not copy system identifiers. + */ + if(junkvar->varoattno > 0) + { + datum[junkvar->varoattno - 1] = slot_getattr(planSlot, + tle->resno, + &(isnull[junkvar->varoattno - 1])); + }
For future reference, you mustn't just assume that a resjunk Var is the same
resjunk Var you added for this purpose. The target list has many consumers,
present and future, so you need to find your resjunk entries more-reliably
than this. See other resjunk-adding code for examples. This concern goes
away if you borrow the "wholerow" approach from INSTEAD OF triggers.
Thanks,
nm
--
Noah Misch
EnterpriseDB http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Thank you very much for this review.
The need here is awfully similar to a need of INSTEAD OF triggers on views.
For them, we add a single "wholerow" resjunk TLE. Is there a good reason to
do it differently for triggers on foreign tables?
I wasn't aware of that, it makes for some much cleaner code IMO. Thanks.
- for after triggers, the whole queuing mechanism is bypassed for foreign
tables. This is IMO acceptable, since foreign tables cannot have
constraints or constraints triggers, and thus have not need for
deferrable execution. This design avoids the need for storing and
retrieving/identifying remote tuples until the query or transaction end.Whether an AFTER ROW trigger is deferred determines whether it runs at the
end of the firing query or at the end of the firing query's transaction.
In all cases, every BEFORE ROW trigger of a given query fires before any
AFTER ROW trigger of the same query. SQL requires that. This proposal
would give foreign table AFTER ROW triggers a novel firing time; let's not
do that.I think the options going forward are either (a) design a way to queue
foreign table AFTER ROW triggers such that we can get the old and/or new
rows at the end of the query or (b) not support AFTER ROW triggers on
foreign tables for the time being.
I did not know this was mandated by the standard.
The attached patch tries to solve this problem by allocating a tuplestore
in the global afterTriggers structure. This tuplestore is used for the whole
transaction, and uses only work_mem per transaction.
Both old and new tuples are stored in this tuplestore. Some additional
bookkeeping is done on the afterTriggers global structure, to keep track of
the number of inserted tuples, and the current read pointer position. The
tuples are identified by their order of insertion during the transaction.
I think this could benefit from some support in the tuplestore API, by
allowing arbitrary seek without the need to store more ReadPointers.
I initially tried to keep track of them by allocating read pointers on the
tuple store, but it turned out to be so expensive that I had to find another
way (24bytes per stored tuple, which are not reclaimable until the end of the
transaction).
What do you think about this approach ? Is there something I missed which
would make it not sustainable ?
If you prefer, I also have a patch implementing the rest of the changes and
keeping the previous behaviour for after triggers.
It's not clear to me whether SQL/MED contemplates triggers on foreign
tables. Its <drop basic column definition> General Rules do mention the
possibility of a reference from a trigger column list. On the other hand,
I see nothing overriding the fact that CREATE TRIGGER only targets base
tables. Is this clearer to anyone else? (This is a minor point, mainly
bearing on the Compatibility section of the CREATE TRIGGER documentation.)
I do not have access to the standard specification, any advice regarding
specs compliance would be welcomed.
- the duplicated resjunk attributes are identified by being:
- marked as resjunk in the targetlist
- not being system or whole-row attributes (varno > 0)There is still one small issue with the attached patch: modifications to
the tuple performed by the foreign data wrapper (via the returned
TupleTableSlot in ExecForeignUpdate and ExecForeignInsert hooks) are not
visible to the AFTER trigger. This could be fixed by merging the planslot
containing the resjunk columns with the returned slot before calling the
trigger, but I'm not really sure how to safely perform that. Any advice ?Currently, FDWs are permitted to skip returning columns not actually
referenced by any RETURNING clause. I would change that part of the API
contract to require returning all columns when an AFTER ROW trigger is
involved. You can't get around doing that by merging old column values,
because, among other reasons, an INSERT does not have those values at all.
I'm not sure this should be part of the API contract: it would make
implementing a FDW more complicated than it is now. The attached patch hooks
on rewriteTargetListIU to add the missing targets to the returning clause,
when needed.
This also changes the way the query's hasReturning flag is set to exclude the
case when only resjunk entries are present in the returning list.
+NOTICE: TG_NARGS: 2 +NOTICE: TG_ARGV: [23, skidoo] +NOTICE: OLD: (11,"bye remote") +insert into rem1 values(1,'insert');Would you trim the verbosity a bit? Maybe merge several of the TG_ fields
onto one line, and remove the low-importance ones. Perhaps issue one line
like this in place of all the current TG_ lines:NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
Fixed in the attached patch.
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON ft1
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER
trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON ft1 FOR EACH
STATEMENT EXECUTE PROCEDURE trigger_func();No test actually fires these triggers.
These should have been placed on the rem1 foreign table, not ft1. Fixed.
+ On foreign tables, triggers can not be defined at row level.
This is obsolete.
I missed that from the earlier version of the patch, thank you.
Why permit some variants, but not every variant, of ALTER TABLE t ENABLE
TRIGGER <type> on foreign tables?
I overlooked that. Fixed.
Keep the old variable name. Exceptions can be made if the name was
deceptive, but "tupleslot" communicates the same thing as "slot".
Fixed.
@@ -2146,7 +2183,8 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo
*relinfo)>
bool
ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,ResultRelInfo *relinfo,
- ItemPointer tupleid) + ItemPointer tupleid, + TupleTableSlot *tupleslot)The new argument is unused.
I added it here for coherence. Now removed in the attached patch.
Please don't change unrelated whitespace.
Please use pgindent to fix the formatting of your new code. It's fine to
introduce occasional whitespace errors, but they're unusually-plentiful
here.
I think its done now. One problem I have with running pgindent is that I
accidentally add chunks that were modified only by pgindent.
Obsolete comment. That's done elsewhere, not here.
Ok
For future reference, you mustn't just assume that a resjunk Var is the same
resjunk Var you added for this purpose. The target list has many
consumers, present and future, so you need to find your resjunk entries
more-reliably than this. See other resjunk-adding code for examples. This
concern goes away if you borrow the "wholerow" approach from INSTEAD OF
triggers.
Using the wholerow approach, the entry is identified by the junkfilter
jf_junkAttNo attribute. So this concern indeed goes away.
Again, thank you for this review.
Attachments:
foreign_trigger_v5.patchtext/x-patch; charset=UTF-8; name=foreign_trigger_v5.patchDownload
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 38c6cf8..8665ade 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2372,3 +2372,307 @@ select * from rem1;
11 | bye remote
(4 rows)
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %', tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+delete from rem1;
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = AFTER, level = STATEMENT
+insert into rem1 values(1,'insert');
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = AFTER, level = STATEMENT
+update rem1 set f2 = 'update' where f1 = 1;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+update rem1 set f2 = f2 || f2;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+DELETE from rem1;
+-- Test WHEN conditions
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+NOTICE: trig_row_before_delete(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+NOTICE: trig_row_after_delete(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+-- Test various RETURN statements in BEFORE triggers.
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------
+ insert triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+ 2 | insert triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------
+ 1 | triggered !
+ 2 | triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------
+ skidoo triggered !
+ skidoo triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | skidoo triggered !
+ 2 | skidoo triggered !
+(2 rows)
+
+DELETE FROM rem1;
+-- Add a second trigger, to check that the changes are propagated corretly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------------------
+ insert triggered ! triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+ 2 | insert triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------
+ 1 | triggered ! triggered !
+ 2 | triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------------------
+ skidoo triggered ! triggered !
+ skidoo triggered ! triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | skidoo triggered ! triggered !
+ 2 | skidoo triggered ! triggered !
+(2 rows)
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+DELETE from rem1;
+INSERT INTO rem1 VALUES (1, 'test');
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+UPDATE rem1 SET f2 = 'test2';
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DELETE from rem1;
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1(f2) VALUES ('test');
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (12,test)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (12,"test triggered !")
+UPDATE rem1 SET f2 = 'testo';
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,testo)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,"testo triggered !")
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index ce8bb75..57cfc2a 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -384,3 +384,212 @@ insert into loc1(f2) values('bye');
insert into rem1(f2) values('bye remote');
select * from loc1;
select * from rem1;
+
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %', tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+delete from rem1;
+insert into rem1 values(1,'insert');
+update rem1 set f2 = 'update' where f1 = 1;
+update rem1 set f2 = f2 || f2;
+
+
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+
+DELETE from rem1;
+
+
+-- Test WHEN conditions
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+
+
+-- Test various RETURN statements in BEFORE triggers.
+
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DELETE FROM rem1;
+
+-- Add a second trigger, to check that the changes are propagated corretly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+
+DELETE from rem1;
+
+INSERT INTO rem1 VALUES (1, 'test');
+
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+
+SELECT * from loc1;
+
+UPDATE rem1 SET f2 = 'test2';
+
+SELECT * from loc1;
+
+DELETE from rem1;
+
+SELECT * from loc1;
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1(f2) VALUES ('test');
+UPDATE rem1 SET f2 = 'testo';
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index e5ec738..07bc757 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -93,7 +93,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<para>
The following table summarizes which types of triggers may be used on
- tables and views:
+ tables, views and foreign tables:
</para>
<informaltable id="supported-trigger-types">
@@ -110,8 +110,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>BEFORE</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -121,8 +121,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>AFTER</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -244,7 +244,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
<term><replaceable class="parameter">table_name</replaceable></term>
<listitem>
<para>
- The name (optionally schema-qualified) of the table or view the trigger
+ The name (optionally schema-qualified) of the table, view or foreign table the trigger
is for.
</para>
</listitem>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index f579340..f115bdc 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -33,11 +33,11 @@
<para>
A trigger is a specification that the database should automatically
execute a particular function whenever a certain type of operation is
- performed. Triggers can be attached to both tables and views.
+ performed. Triggers can be attached to tables, views and foreign tables.
</para>
<para>
- On tables, triggers can be defined to execute either before or after any
+ On tables and foreign tables, triggers can be defined to execute either before or after any
<command>INSERT</command>, <command>UPDATE</command>, or
<command>DELETE</command> operation, either once per modified row,
or once per <acronym>SQL</acronym> statement.
@@ -46,7 +46,7 @@
<command>UPDATE</command> statement.
Triggers can also fire for <command>TRUNCATE</command> statements.
If a trigger event occurs, the trigger's function is called at the
- appropriate time to handle the event.
+ appropriate time to handle the event. Foreign tables do not support the TRUNCATE statement at all.
</para>
<para>
@@ -108,11 +108,11 @@
statement starts to do anything, while statement-level <literal>AFTER</>
triggers fire at the very end of the statement. These types of
triggers may be defined on tables or views. Row-level <literal>BEFORE</>
- triggers fire immediately before a particular row is operated on,
- while row-level <literal>AFTER</> triggers fire at the end of the
- statement (but before any statement-level <literal>AFTER</> triggers).
- These types of triggers may only be defined on tables. Row-level
- <literal>INSTEAD OF</> triggers may only be defined on views, and fire
+ triggers fire immediately before a particular row is operated on.
+ For regular and foreign tables, row-level <literal>AFTER</> triggers fire at
+ the end of the statement (but before any statement-level <literal>AFTER</> triggers).
+ These types of triggers may only be defined on tables and foreign tables.
+ Row-level <literal>INSTEAD OF</> triggers may only be defined on views, and fire
immediately as each row in the view is identified as needing to be
operated on.
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 26a4613..e48a3f4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3157,6 +3157,9 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DisableTrig: /* DISABLE TRIGGER variants */
case AT_DisableTrigAll:
case AT_DisableTrigUser:
+ ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE);
+ pass = AT_PASS_MISC;
+ break;
case AT_EnableRule: /* ENABLE/DISABLE RULE variants */
case AT_EnableAlwaysRule:
case AT_EnableReplicaRule:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 86449a6..882b57d 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -55,6 +55,7 @@
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/tqual.h"
+#include "utils/tuplestore.h"
/* GUC variables */
@@ -75,6 +76,9 @@ static HeapTuple GetTupleForTrigger(EState *estate,
ItemPointer tid,
LockTupleMode lockmode,
TupleTableSlot **newSlot);
+
+static HeapTuple ExtractOldTuple(TupleTableSlot *mixedtupleslot,
+ ResultRelInfo *relinfo);
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
Trigger *trigger, TriggerEvent event,
Bitmapset *modifiedCols,
@@ -184,12 +188,22 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
RelationGetRelationName(rel)),
errdetail("Views cannot have TRUNCATE triggers.")));
}
+ else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ 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("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
-
+ }
if (!allowSystemTableMods && IsSystemRelation(rel))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1062,10 +1076,11 @@ RemoveTriggerById(Oid trigOid)
rel = heap_open(relid, AccessExclusiveLock);
if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_VIEW)
+ rel->rd_rel->relkind != RELKIND_VIEW &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view",
+ errmsg("\"%s\" is not a table, view or foreign table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
@@ -1166,10 +1181,11 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
form = (Form_pg_class) GETSTRUCT(tuple);
/* only tables and views can have triggers */
- if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW)
+ if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW &&
+ form->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view", rv->relname)));
+ errmsg("\"%s\" is not a table, view or foreign table", rv->relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
@@ -1844,9 +1860,7 @@ ExecCallTriggerFunc(TriggerData *trigdata,
*/
InitFunctionCallInfoData(fcinfo, finfo, 0,
InvalidOid, (Node *) trigdata, NULL);
-
pgstat_init_function_usage(&fcinfo, &fcusage);
-
MyTriggerDepth++;
PG_TRY();
{
@@ -2155,9 +2169,18 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
HeapTuple newtuple;
TupleTableSlot *newSlot;
int i;
+ Relation rel = relinfo->ri_RelationDesc;
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- LockTupleExclusive, &newSlot);
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ LockTupleExclusive, &newSlot);
+ }
if (trigtuple == NULL)
return false;
@@ -2204,19 +2227,33 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
void
ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ TupleTableSlot *slot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since
+ * there cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ trigtuple = ExtractOldTuple(slot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ }
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
true, trigtuple, NULL, NIL, NULL);
heap_freetuple(trigtuple);
@@ -2335,7 +2372,8 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TupleTableSlot *
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid, TupleTableSlot *slot)
+ ItemPointer tupleid,
+ TupleTableSlot *slot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
HeapTuple slottuple = ExecMaterializeSlot(slot);
@@ -2345,6 +2383,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
HeapTuple oldtuple;
TupleTableSlot *newSlot;
int i;
+ Relation relation = relinfo->ri_RelationDesc;
Bitmapset *modifiedCols;
Bitmapset *keyCols;
LockTupleMode lockmode;
@@ -2355,7 +2394,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
* concurrency.
*/
modifiedCols = GetModifiedColumns(relinfo, estate);
- keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
+ keyCols = RelationGetIndexAttrBitmap(relation,
INDEX_ATTR_BITMAP_KEY);
if (bms_overlap(keyCols, modifiedCols))
lockmode = LockTupleExclusive;
@@ -2363,8 +2402,16 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
lockmode = LockTupleNoKeyExclusive;
/* get a copy of the on-disk tuple we are planning to update */
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- lockmode, &newSlot);
+ if (relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ lockmode, &newSlot);
+ }
if (trigtuple == NULL)
return NULL; /* cancel the update action */
@@ -2446,24 +2493,40 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid, HeapTuple newtuple,
+ ItemPointer tupleid,
+ HeapTuple newtuple,
+ TupleTableSlot *slot,
List *recheckIndexes)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since
+ * there cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ trigtuple = ExtractOldTuple(slot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ }
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
true, trigtuple, newtuple, recheckIndexes,
GetModifiedColumns(relinfo, estate));
heap_freetuple(trigtuple);
+
}
}
@@ -2731,6 +2794,31 @@ ltrmark:;
}
/*
+ * Get an old tuple from a "mixed tuple", containing both the new values as well
+ * as well as the old ones as resjunk columns.
+ */
+static HeapTuple
+ExtractOldTuple(TupleTableSlot *planSlot,
+ ResultRelInfo *relinfo)
+{
+ bool isNull;
+ JunkFilter *junkfilter = relinfo->ri_junkFilter;
+ HeapTuple oldtuple = palloc0(sizeof(HeapTupleData));
+ HeapTupleHeader td;
+ Datum datum = ExecGetJunkAttribute(planSlot,
+ junkfilter->jf_junkAttNo,
+ &isNull);
+
+ /* shouldn't ever get a null result... */
+ if (isNull)
+ elog(ERROR, "wholerow is NULL");
+ td = DatumGetHeapTupleHeader(datum);
+ (*oldtuple).t_len = HeapTupleHeaderGetDatumLength(td);
+ (*oldtuple).t_data = td;
+ return oldtuple;
+}
+
+/*
* Is trigger enabled to fire?
*/
static bool
@@ -2948,6 +3036,7 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_2CTIDS 0x10000000
#define AFTER_TRIGGER_DONE 0x20000000
#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
+#define AFTER_TRIGGER_FDW 0x80000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -2975,9 +3064,18 @@ typedef struct AfterTriggerEventDataOneCtid
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
} AfterTriggerEventDataOneCtid;
+typedef struct AfterTriggerEventDataFDW
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ int ate_ptr1; /* inserted, deleted, or old updated tuple */
+ int ate_ptr2; /* new updated tuple */
+} AfterTriggerEventDataFDW;
+
#define SizeofTriggerEvent(evt) \
(((evt)->ate_flags & AFTER_TRIGGER_2CTIDS) ? \
- sizeof(AfterTriggerEventData) : sizeof(AfterTriggerEventDataOneCtid))
+ sizeof(AfterTriggerEventData) : \
+ ((evt)->ate_flags & AFTER_TRIGGER_FDW) ? sizeof(AfterTriggerEventDataFDW) : \
+ sizeof(AfterTriggerEventDataOneCtid))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3071,6 +3169,9 @@ typedef struct AfterTriggerEventList
* each stack. (By not keeping our own stack pointer, we can avoid trouble
* in cases where errors during subxact abort cause multiple invocations
* of AfterTriggerEndSubXact() at the same nesting depth.)
+ *
+ * fdwtuplestore is used to store references for FDW tuples, since they cannot
+ * be retrieved normally.
*/
typedef struct AfterTriggersData
{
@@ -3089,6 +3190,9 @@ typedef struct AfterTriggersData
int *depth_stack; /* stacked query_depths */
CommandId *firing_stack; /* stacked firing_counters */
int maxtransdepth; /* allocated len of above arrays */
+ Tuplestorestate *fdwtuplestore;
+ int fdw_nextwrite;
+ int fdw_lastread;
} AfterTriggersData;
typedef AfterTriggersData *AfterTriggers;
@@ -3100,7 +3204,8 @@ static void AfterTriggerExecute(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
- MemoryContext per_tuple_context);
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot);
static SetConstraintState SetConstraintStateCreate(int numalloc);
static SetConstraintState SetConstraintStateCopy(SetConstraintState state);
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
@@ -3353,7 +3458,8 @@ static void
AfterTriggerExecute(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
- MemoryContext per_tuple_context)
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot)
{
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
@@ -3390,34 +3496,72 @@ AfterTriggerExecute(AfterTriggerEvent event,
/*
* Fetch the required tuple(s).
*/
- if (ItemPointerIsValid(&(event->ate_ctid1)))
+ if (event->ate_flags & AFTER_TRIGGER_FDW)
{
- ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) event;
+
+ if (afterTriggers->fdw_lastread > fdwevent->ate_ptr1)
+ {
+ tuplestore_rescan(afterTriggers->fdwtuplestore);
+ afterTriggers->fdw_lastread = 0;
+ }
+ while (afterTriggers->fdw_lastread < fdwevent->ate_ptr1)
+ {
+ tuplestore_advance(afterTriggers->fdwtuplestore, true);
+ afterTriggers->fdw_lastread++;
+ }
+ if (!tuplestore_gettupleslot(afterTriggers->fdwtuplestore, true, false, trig_tuple_slot))
+ {
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
- LocTriggerData.tg_trigtuple = &tuple1;
- LocTriggerData.tg_trigtuplebuf = buffer1;
- }
- else
- {
- LocTriggerData.tg_trigtuple = NULL;
+ }
+ afterTriggers->fdw_lastread++;
+ LocTriggerData.tg_trigtuple = ExecCopySlotTuple(trig_tuple_slot);
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
- }
- /* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) &&
- ItemPointerIsValid(&(event->ate_ctid2)))
- {
- ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
- elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
- LocTriggerData.tg_newtuple = &tuple2;
- LocTriggerData.tg_newtuplebuf = buffer2;
+ if (fdwevent->ate_ptr2 >= 0)
+ {
+ /* The new tuple necessarily follows the previous one */
+ if (!tuplestore_gettupleslot(afterTriggers->fdwtuplestore, true, false, trig_tuple_slot))
+ {
+ elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+ }
+ afterTriggers->fdw_lastread = afterTriggers->fdw_lastread + 1;
+ LocTriggerData.tg_newtuple = ExecCopySlotTuple(trig_tuple_slot);
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
+ tuplestore_select_read_pointer(afterTriggers->fdwtuplestore, 0);
}
else
{
- LocTriggerData.tg_newtuple = NULL;
- LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ if (ItemPointerIsValid(&(event->ate_ctid1)))
+ {
+ ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
+ elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+ LocTriggerData.tg_trigtuple = &tuple1;
+ LocTriggerData.tg_trigtuplebuf = buffer1;
+ }
+ else
+ {
+ LocTriggerData.tg_trigtuple = NULL;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ }
+
+ /* don't touch ctid2 if not there */
+ if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) &&
+ ItemPointerIsValid(&(event->ate_ctid2)))
+ {
+ ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
+ elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+ LocTriggerData.tg_newtuple = &tuple2;
+ LocTriggerData.tg_newtuplebuf = buffer2;
+ }
+ else
+ {
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
}
/*
@@ -3559,6 +3703,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
TriggerDesc *trigdesc = NULL;
FmgrInfo *finfo = NULL;
Instrumentation *instr = NULL;
+ TupleTableSlot *slot = NULL;
/* Make a local EState if need be */
if (estate == NULL)
@@ -3574,7 +3719,6 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
-
for_each_chunk(chunk, *events)
{
AfterTriggerEvent event;
@@ -3603,6 +3747,11 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
trigdesc = rInfo->ri_TrigDesc;
finfo = rInfo->ri_TrigFunctions;
instr = rInfo->ri_TrigInstrument;
+ /* Make a slot to read back tuple from the tuplestore */
+ if (slot != NULL)
+ ExecDropSingleTupleTableSlot(slot);
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ slot = MakeSingleTupleTableSlot(rel->rd_att);
if (trigdesc == NULL) /* should not happen */
elog(ERROR, "relation %u has no triggers",
evtshared->ats_relid);
@@ -3614,7 +3763,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
* won't try to re-fire it.
*/
AfterTriggerExecute(event, rel, trigdesc, finfo, instr,
- per_tuple_context);
+ per_tuple_context, slot);
/*
* Mark the event as done.
@@ -3645,6 +3794,9 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
events->tailfree = chunk->freeptr;
}
}
+ if (slot != NULL)
+ ExecDropSingleTupleTableSlot(slot);
+
/* Release working resources */
MemoryContextDelete(per_tuple_context);
@@ -3709,6 +3861,9 @@ AfterTriggerBeginXact(void)
afterTriggers->depth_stack = NULL;
afterTriggers->firing_stack = NULL;
afterTriggers->maxtransdepth = 0;
+ afterTriggers->fdwtuplestore = tuplestore_begin_heap(true, false, work_mem);
+ afterTriggers->fdw_nextwrite = 0;
+ afterTriggers->fdw_lastread = 0;
}
@@ -3751,6 +3906,7 @@ AfterTriggerBeginQuery(void)
events->head = NULL;
events->tail = NULL;
events->tailfree = NULL;
+
}
@@ -3900,8 +4056,14 @@ AfterTriggerEndXact(bool isCommit)
* of memory for the list!
*/
if (afterTriggers && afterTriggers->event_cxt)
+ {
MemoryContextDelete(afterTriggers->event_cxt);
-
+ }
+ if (afterTriggers && afterTriggers->fdwtuplestore)
+ {
+ tuplestore_end(afterTriggers->fdwtuplestore);
+ afterTriggers->fdwtuplestore = NULL;
+ }
afterTriggers = NULL;
}
@@ -4528,6 +4690,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
+ char relkind = relinfo->ri_RelationDesc->rd_rel->relkind;
int tgtype_event;
int tgtype_level;
int i;
@@ -4550,6 +4713,10 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* arrays.
*/
new_event.ate_flags = 0;
+ if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ {
+ new_event.ate_flags = AFTER_TRIGGER_FDW;
+ }
switch (event)
{
case TRIGGER_EVENT_INSERT:
@@ -4558,8 +4725,20 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup == NULL);
Assert(newtup != NULL);
- ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid1));
- ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) & new_event;
+
+ tuplestore_puttuple(afterTriggers->fdwtuplestore, newtup);
+ fdwevent->ate_ptr1 = afterTriggers->fdw_nextwrite;
+ afterTriggers->fdw_nextwrite++;
+ fdwevent->ate_ptr2 = -1;
+ }
+ else
+ {
+ ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ }
}
else
{
@@ -4575,8 +4754,21 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup != NULL);
Assert(newtup == NULL);
- ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
- ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) & new_event;
+
+ tuplestore_puttuple(afterTriggers->fdwtuplestore, oldtup);
+ fdwevent->ate_ptr1 = afterTriggers->fdw_nextwrite;
+ afterTriggers->fdw_nextwrite++;
+
+ fdwevent->ate_ptr2 = -1;
+ }
+ else
+ {
+ ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ }
}
else
{
@@ -4592,9 +4784,24 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup != NULL);
Assert(newtup != NULL);
- ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
- ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
- new_event.ate_flags |= AFTER_TRIGGER_2CTIDS;
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) & new_event;
+
+ tuplestore_puttuple(afterTriggers->fdwtuplestore, oldtup);
+ fdwevent->ate_ptr1 = afterTriggers->fdw_nextwrite;
+ afterTriggers->fdw_nextwrite++;
+ tuplestore_puttuple(afterTriggers->fdwtuplestore, newtup);
+ fdwevent->ate_ptr2 = afterTriggers->fdw_nextwrite;
+ afterTriggers->fdw_nextwrite++;
+
+ }
+ else
+ {
+ ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
+ new_event.ate_flags |= AFTER_TRIGGER_2CTIDS;
+ }
}
else
{
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 6f0f47e..4553f4e 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -488,7 +488,7 @@ ldelete:;
(estate->es_processed)++;
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, planSlot);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
@@ -788,7 +788,7 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
+ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, planSlot,
recheckIndexes);
list_free(recheckIndexes);
@@ -986,7 +986,18 @@ ExecModifyTable(ModifyTableState *node)
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
- /* do nothing; FDW must fetch any junk attrs it wants */
+ /*
+ * If the junkAttNo is valid, then it identifies the
+ * wholerow attribute. This is the case when there is an
+ * UPDATE or DELETE trigger.
+ */
+ if (AttributeNumberIsValid(junkfilter->jf_junkAttNo))
+ {
+ datum = ExecGetJunkAttribute(slot,
+ junkfilter->jf_junkAttNo,
+ &isNull);
+ oldtuple = DatumGetHeapTupleHeader(datum);
+ }
}
else
{
@@ -1334,7 +1345,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
- /* FDW must fetch any junk attrs it wants */
+ /*
+ * FDW must fetch any junk attrs it wants When there
+ * is an AFTER trigger, there should be a wholerow
+ * attribute.
+ */
+ AttrNumber junkAttNo = ExecFindJunkAttribute(j, "wholerow");
+
+ if (AttributeNumberIsValid(junkAttNo))
+ j->jf_junkAttNo = junkAttNo;
}
else
{
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 35bda67..52cc5b7 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -240,7 +240,22 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->commandType = parse->commandType;
result->queryId = parse->queryId;
- result->hasReturning = (parse->returningList != NIL);
+
+ /*
+ * Mark the result as having RETURNING only if the returning target list
+ * has non-resjunk entries
+ */
+ result->hasReturning = false;
+ foreach(lp, parse->returningList)
+ {
+ TargetEntry *tle = lfirst(lp);
+
+ if (!tle->resjunk)
+ {
+ result->hasReturning = true;
+ break;
+ }
+ }
result->hasModifyingCTE = parse->hasModifyingCTE;
result->canSetTag = parse->canSetTag;
result->transientPlan = glob->transientPlan;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 0b13645..cd18a5a 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -642,6 +642,10 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
* 4. Sort the tlist into standard order: non-junk fields in order by resno,
* then junk fields (these in no particular order).
*
+ * 5. For an insert on a foreign table with an after trigger, add missing
+ * attribute to the returning targetlist. This is needed to ensure that all
+ * attributes are fetched from the remote side on a returning statement.
+ *
* We must do items 1,2,3 before firing rewrite rules, else rewritten
* references to NEW.foo will produce wrong or incomplete results. Item 4
* is not needed for rewriting, but will be needed by the planner, and we
@@ -657,14 +661,17 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
List **attrno_list)
{
CmdType commandType = parsetree->commandType;
- TargetEntry **new_tles;
+ TargetEntry **new_tles,
+ **returning_tles;
List *new_tlist = NIL;
List *junk_tlist = NIL;
Form_pg_attribute att_tup;
int attrno,
next_junk_attrno,
- numattrs;
+ numattrs,
+ next_junk_returningattrno;
ListCell *temp;
+ bool need_full_returning_tlist = false;
if (attrno_list) /* initialize optional result list */
*attrno_list = NIL;
@@ -681,6 +688,7 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
numattrs = RelationGetNumberOfAttributes(target_relation);
new_tles = (TargetEntry **) palloc0(numattrs * sizeof(TargetEntry *));
next_junk_attrno = numattrs + 1;
+ next_junk_returningattrno = list_length(parsetree->returningList);
foreach(temp, parsetree->targetList)
{
@@ -730,6 +738,45 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
}
}
+ /*
+ * For a foreign table with a matching AFTER trigger, we need the full
+ * returning tlist.
+ */
+ need_full_returning_tlist =
+ target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE &&
+ target_relation->trigdesc &&
+ ((commandType == CMD_INSERT && target_relation->trigdesc->trig_insert_after_row) ||
+ (commandType == CMD_UPDATE && target_relation->trigdesc->trig_update_after_row));
+
+ /*
+ * For foreign tables, build a similar array for returning tlist.
+ */
+ if (need_full_returning_tlist)
+ {
+ returning_tles = (TargetEntry **) palloc0(numattrs * sizeof(TargetEntry *));
+ foreach(temp, parsetree->returningList)
+ {
+ TargetEntry *old_rtle = (TargetEntry *) lfirst(temp);
+
+ if (IsA(old_rtle->expr, Var))
+ {
+ Var *var = (Var *) old_rtle->expr;
+
+ if (var->varno == parsetree->resultRelation)
+ {
+ attrno = var->varattno;
+ if (attrno < 1 || attrno > numattrs)
+ elog(ERROR, "bogus resno %d in targetlist", attrno);
+ att_tup = target_relation->rd_att->attrs[attrno - 1];
+ /* We can (and must) ignore deleted attributes */
+ if (att_tup->attisdropped)
+ continue;
+ returning_tles[attrno - 1] = old_rtle;
+ }
+ }
+ }
+ }
+
for (attrno = 1; attrno <= numattrs; attrno++)
{
TargetEntry *new_tle = new_tles[attrno - 1];
@@ -790,6 +837,24 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
false);
}
+ if (need_full_returning_tlist && returning_tles[attrno - 1] == NULL)
+ {
+ TargetEntry *returning_tle = returning_tles[attrno - 1];
+ Node *new_expr = (Node *) makeVar(parsetree->resultRelation,
+ attrno,
+ att_tup->atttypid,
+ att_tup->atttypmod,
+ att_tup->attcollation,
+ 0);
+
+ next_junk_returningattrno++;
+ returning_tle = makeTargetEntry((Expr *) new_expr,
+ next_junk_returningattrno,
+ pstrdup(NameStr(att_tup->attname)),
+ true);
+ parsetree->returningList = lappend(parsetree->returningList, returning_tle);
+ }
+
/*
* For an UPDATE on a trigger-updatable view, provide a dummy entry
* whenever there is no explicit assignment.
@@ -818,7 +883,10 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
}
pfree(new_tles);
-
+ if (need_full_returning_tlist)
+ {
+ pfree(returning_tles);
+ }
parsetree->targetList = list_concat(new_tlist, junk_tlist);
}
@@ -1174,7 +1242,7 @@ static void
rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
Relation target_relation)
{
- Var *var;
+ Var *var = NULL;
const char *attrname;
TargetEntry *tle;
@@ -1206,7 +1274,27 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
target_relation);
- return;
+ /*
+ * If we have a trigger corresponding to the operation, add a wholerow
+ * attribute.
+ */
+ if (target_relation->trigdesc &&
+ ((parsetree->commandType == CMD_UPDATE &&
+ (target_relation->trigdesc->trig_update_after_row
+ || target_relation->trigdesc->trig_update_before_row)) ||
+ (parsetree->commandType == CMD_DELETE &&
+ (target_relation->trigdesc->trig_delete_after_row ||
+ target_relation->trigdesc->trig_delete_before_row))))
+ {
+ var = makeWholeRowVar(target_rte,
+ parsetree->resultRelation,
+ 0,
+ false);
+
+ attrname = "wholerow";
+
+ }
+
}
else
{
@@ -1221,13 +1309,15 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
attrname = "wholerow";
}
+ if (var != NULL)
+ {
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->targetList) + 1,
+ pstrdup(attrname),
+ true);
- tle = makeTargetEntry((Expr *) var,
- list_length(parsetree->targetList) + 1,
- pstrdup(attrname),
- true);
-
- parsetree->targetList = lappend(parsetree->targetList, tle);
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+ }
}
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 44d686c..ed598f0 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -150,7 +150,8 @@ extern bool ExecBRDeleteTriggers(EState *estate,
ItemPointer tupleid);
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ TupleTableSlot *slot);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -167,6 +168,7 @@ extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple newtuple,
+ TupleTableSlot *slot,
List *recheckIndexes);
extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 60506e0..3405b6c 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -1158,6 +1158,36 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
ERROR: must be owner of foreign server s9
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+ERROR: "foreign_table_1" is a foreign table
+DETAIL: Foreign Tables cannot have constraint triggers.
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+DROP FUNCTION dummy_trigger();
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
ERROR: foreign table "no_table" does not exist
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index f819eb1..6166097 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -470,6 +470,43 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+
+DROP FUNCTION dummy_trigger();
+
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
DROP FOREIGN TABLE IF EXISTS no_table;
Hello,
I initially tried to keep track of them by allocating read pointers on the
tuple store, but it turned out to be so expensive that I had to find another
way (24bytes per stored tuple, which are not reclaimable until the end of
the transaction).What do you think about this approach ? Is there something I missed which
would make it not sustainable ?
It seems to me reasonable approach to track them.
Just a corner case, it may cause an unexpected problem if someone tried to
update a foreign table with 2^31 of tuples because of int index.
It may make sense to put a check fdw_nextwrite is less than INT_MAX. :-)
I have a few other minor comments:
+static HeapTuple
+ExtractOldTuple(TupleTableSlot *planSlot,
+ ResultRelInfo *relinfo)
+{
+ bool isNull;
+ JunkFilter *junkfilter = relinfo->ri_junkFilter;
+ HeapTuple oldtuple = palloc0(sizeof(HeapTupleData));
+ HeapTupleHeader td;
+ Datum datum = ExecGetJunkAttribute(planSlot,
+ junkfilter->jf_junkAttNo,
+ &isNull);
+
+ /* shouldn't ever get a null result... */
+ if (isNull)
+ elog(ERROR, "wholerow is NULL");
+ td = DatumGetHeapTupleHeader(datum);
+ (*oldtuple).t_len = HeapTupleHeaderGetDatumLength(td);
+ (*oldtuple).t_data = td;
+ return oldtuple;
+}
+
Why not usual coding manner as:
oldtuple->t_len = HeapTupleHeaderGetDatumLength(td);
oldtuple->t_data = td;
Also, it don't put tableOid on the tuple.
oldtuple->t_tableOid = RelationGetRelid(relinfo->ri_RelationDesc);
@@ -730,6 +738,45 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
+ /*
+ * For foreign tables, build a similar array for returning tlist.
+ */
+ if (need_full_returning_tlist)
+ {
+ returning_tles = (TargetEntry **) palloc0(numattrs * sizeof(TargetEntry *));
+ foreach(temp, parsetree->returningList)
+ {
+ TargetEntry *old_rtle = (TargetEntry *) lfirst(temp);
+
+ if (IsA(old_rtle->expr, Var))
+ {
+ Var *var = (Var *) old_rtle->expr;
+
+ if (var->varno == parsetree->resultRelation)
+ {
+ attrno = var->varattno;
+ if (attrno < 1 || attrno > numattrs)
+ elog(ERROR, "bogus resno %d in targetlist", attrno);
This checks caused an error when returning list contains a reference to
system column; that has negative attribute number.
Probably, it should be "continue;", instead of elog().
BTW, isn't it sufficient to inhibit optimization by putting whole-row-reference
here, rather than whole-row-reference. Postgres_fdw extracts whole-row-reference
into individual columns reference.
+ att_tup = target_relation->rd_att->attrs[attrno - 1];
+ /* We can (and must) ignore deleted attributes */
+ if (att_tup->attisdropped)
+ continue;
+ returning_tles[attrno - 1] = old_rtle;
+ }
+ }
+ }
+ }
+
Thanks,
--
NEC OSS Promotion Center / PG-Strom Project
KaiGai Kohei <kaigai@ak.jp.nec.com>
-----Original Message-----
From: pgsql-hackers-owner@postgresql.org
[mailto:pgsql-hackers-owner@postgresql.org] On Behalf Of Ronan Dunklau
Sent: Thursday, January 23, 2014 11:18 PM
To: Noah Misch
Cc: pgsql-hackers@postgresql.org
Subject: Re: [HACKERS] Triggers on foreign tablesThank you very much for this review.
The need here is awfully similar to a need of INSTEAD OF triggers on views.
For them, we add a single "wholerow" resjunk TLE. Is there a good
reason to do it differently for triggers on foreign tables?I wasn't aware of that, it makes for some much cleaner code IMO. Thanks.
- for after triggers, the whole queuing mechanism is bypassed for
foreign tables. This is IMO acceptable, since foreign tables cannot
have constraints or constraints triggers, and thus have not need for
deferrable execution. This design avoids the need for storing and
retrieving/identifying remote tuples until the query or transactionend.
Whether an AFTER ROW trigger is deferred determines whether it runs at
the end of the firing query or at the end of the firing query's transaction.
In all cases, every BEFORE ROW trigger of a given query fires before
any AFTER ROW trigger of the same query. SQL requires that. This
proposal would give foreign table AFTER ROW triggers a novel firing
time; let's not do that.I think the options going forward are either (a) design a way to queue
foreign table AFTER ROW triggers such that we can get the old and/or
new rows at the end of the query or (b) not support AFTER ROW triggers
on foreign tables for the time being.I did not know this was mandated by the standard.
The attached patch tries to solve this problem by allocating a tuplestore
in the global afterTriggers structure. This tuplestore is used for the whole
transaction, and uses only work_mem per transaction.Both old and new tuples are stored in this tuplestore. Some additional
bookkeeping is done on the afterTriggers global structure, to keep track
of the number of inserted tuples, and the current read pointer position.
The tuples are identified by their order of insertion during the
transaction.
I think this could benefit from some support in the tuplestore API, by
allowing arbitrary seek without the need to store more ReadPointers.I initially tried to keep track of them by allocating read pointers on the
tuple store, but it turned out to be so expensive that I had to find another
way (24bytes per stored tuple, which are not reclaimable until the end of
the transaction).What do you think about this approach ? Is there something I missed which
would make it not sustainable ?If you prefer, I also have a patch implementing the rest of the changes
and keeping the previous behaviour for after triggers.It's not clear to me whether SQL/MED contemplates triggers on foreign
tables. Its <drop basic column definition> General Rules do mention
the possibility of a reference from a trigger column list. On the
other hand, I see nothing overriding the fact that CREATE TRIGGER only
targets base tables. Is this clearer to anyone else? (This is a
minor point, mainly bearing on the Compatibility section of the CREATE
TRIGGER documentation.)I do not have access to the standard specification, any advice regarding
specs compliance would be welcomed.- the duplicated resjunk attributes are identified by being:
- marked as resjunk in the targetlist
- not being system or whole-row attributes (varno > 0)There is still one small issue with the attached patch:
modifications to the tuple performed by the foreign data wrapper
(via the returned TupleTableSlot in ExecForeignUpdate and
ExecForeignInsert hooks) are not visible to the AFTER trigger. This
could be fixed by merging the planslot containing the resjunk
columns with the returned slot before calling the trigger, but I'm notreally sure how to safely perform that. Any advice ?
Currently, FDWs are permitted to skip returning columns not actually
referenced by any RETURNING clause. I would change that part of the
API contract to require returning all columns when an AFTER ROW
trigger is involved. You can't get around doing that by merging old
column values, because, among other reasons, an INSERT does not have thosevalues at all.
I'm not sure this should be part of the API contract: it would make
implementing a FDW more complicated than it is now. The attached patch hooks
on rewriteTargetListIU to add the missing targets to the returning clause,
when needed.This also changes the way the query's hasReturning flag is set to exclude
the case when only resjunk entries are present in the returning list.+NOTICE: TG_NARGS: 2 +NOTICE: TG_ARGV: [23, skidoo] +NOTICE: OLD: (11,"bye remote") +insert into rem1 values(1,'insert');Would you trim the verbosity a bit? Maybe merge several of the TG_
fields onto one line, and remove the low-importance ones. Perhaps
issue one line like this in place of all the current TG_ lines:NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
Fixed in the attached patch.
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE +ON ft1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON ft1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();No test actually fires these triggers.
These should have been placed on the rem1 foreign table, not ft1. Fixed.
+ On foreign tables, triggers can not be defined at row level.
This is obsolete.
I missed that from the earlier version of the patch, thank you.
Why permit some variants, but not every variant, of ALTER TABLE t
ENABLE TRIGGER <type> on foreign tables?I overlooked that. Fixed.
Keep the old variable name. Exceptions can be made if the name was
deceptive, but "tupleslot" communicates the same thing as "slot".Fixed.
@@ -2146,7 +2183,8 @@ ExecASDeleteTriggers(EState *estate,
ResultRelInfo *relinfo)> bool ExecBRDeleteTriggers(EState *estate,
EPQState *epqstate,ResultRelInfo *relinfo,
- ItemPointer tupleid) + ItemPointer tupleid, + TupleTableSlot *tupleslot)The new argument is unused.
I added it here for coherence. Now removed in the attached patch.
Please don't change unrelated whitespace.
Please use pgindent to fix the formatting of your new code. It's fine
to introduce occasional whitespace errors, but they're
unusually-plentiful here.I think its done now. One problem I have with running pgindent is that I
accidentally add chunks that were modified only by pgindent.Obsolete comment. That's done elsewhere, not here.
Ok
For future reference, you mustn't just assume that a resjunk Var is
the same resjunk Var you added for this purpose. The target list has
many consumers, present and future, so you need to find your resjunk
entries more-reliably than this. See other resjunk-adding code for
examples. This concern goes away if you borrow the "wholerow"
approach from INSTEAD OF triggers.Using the wholerow approach, the entry is identified by the junkfilter
jf_junkAttNo attribute. So this concern indeed goes away.Again, thank you for this review.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Thank you for taking the time to review this. Please find attached a new
version of the patch.
Le mercredi 29 janvier 2014 09:13:36 Kouhei Kaigai a écrit :
It may make sense to put a check fdw_nextwrite is less than INT_MAX. :-)
The attached patch checks this, and add documentation for this limitation.
I'm not really sure about how to phrase that correctly in the error message
and the documentation. One can store at most INT_MAX foreign tuples, which
means that at most INT_MAX insert or delete or "half-updates" can occur. By
half-updates, I mean that for update two tuples are stored whereas in contrast
to only one for insert and delete trigger.
Besides, I don't know where this disclaimer should be in the documentation.
Any advice here ?
Why not usual coding manner as:
oldtuple->t_len = HeapTupleHeaderGetDatumLength(td);
oldtuple->t_data = td;Also, it don't put tableOid on the tuple.
oldtuple->t_tableOid = RelationGetRelid(relinfo->ri_RelationDesc);
Fixed, thank you.
@@ -730,6 +738,45 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation, + /* + * For foreign tables, build a similar array for returning tlist. + */ + if (need_full_returning_tlist) + { + returning_tles = (TargetEntry **) palloc0(numattrs * sizeof(TargetEntry *)); + foreach(temp, parsetree->returningList) + { + TargetEntry *old_rtle = (TargetEntry *) lfirst(temp); + + if (IsA(old_rtle->expr, Var)) + { + Var *var = (Var *) old_rtle->expr; + + if (var->varno == parsetree->resultRelation) + { + attrno = var->varattno; + if (attrno < 1 || attrno > numattrs) + elog(ERROR, "bogus resno %d in targetlist", attrno);This checks caused an error when returning list contains a reference to
system column; that has negative attribute number.
Probably, it should be "continue;", instead of elog().
Are system attributes supposed to be accessible for foreign tables? I think
they only make sense for postgres_fdw.
Anyway, I fixed this and added a test case returning ctid, xmin and xmax.
BTW, isn't it sufficient to inhibit optimization by putting
whole-row-reference here, rather than whole-row-reference. Postgres_fdw
extracts whole-row-reference into individual columns reference.
The code is more straightforward with a whole-row reference. Done in the
attached patch.
--
Ronan Dunklau
http://dalibo.com - http://dalibo.org
Attachments:
foreign_trigger_v6.patchtext/x-patch; charset=UTF-8; name=foreign_trigger_v6.patchDownload
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 38c6cf8..f77144f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2372,3 +2372,318 @@ select * from rem1;
11 | bye remote
(4 rows)
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %', tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+delete from rem1;
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = AFTER, level = STATEMENT
+insert into rem1 values(1,'insert');
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = AFTER, level = STATEMENT
+update rem1 set f2 = 'update' where f1 = 1;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+update rem1 set f2 = f2 || f2;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+DELETE from rem1;
+-- Test WHEN conditions
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+NOTICE: trig_row_before_delete(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+NOTICE: trig_row_after_delete(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+-- Test various RETURN statements in BEFORE triggers.
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------
+ insert triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+ 2 | insert triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------
+ 1 | triggered !
+ 2 | triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------
+ skidoo triggered !
+ skidoo triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | skidoo triggered !
+ 2 | skidoo triggered !
+(2 rows)
+
+DELETE FROM rem1;
+-- Add a second trigger, to check that the changes are propagated corretly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------------------
+ insert triggered ! triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+ 2 | insert triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------
+ 1 | triggered ! triggered !
+ 2 | triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------------------
+ skidoo triggered ! triggered !
+ skidoo triggered ! triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | skidoo triggered ! triggered !
+ 2 | skidoo triggered ! triggered !
+(2 rows)
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+DELETE from rem1;
+INSERT INTO rem1 VALUES (1, 'test');
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+UPDATE rem1 SET f2 = 'test2';
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DELETE from rem1;
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1(f2) VALUES ('test');
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (12,test)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (12,"test triggered !")
+UPDATE rem1 SET f2 = 'testo';
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,testo)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,"testo triggered !")
+-- Test returning system attributes
+INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax;
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (13,test)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (13,"test triggered !")
+ ctid | xmin | xmax
+--------+------+------------
+ (0,27) | 180 | 4294967295
+(1 row)
+
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index ce8bb75..54b35d9 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -384,3 +384,215 @@ insert into loc1(f2) values('bye');
insert into rem1(f2) values('bye remote');
select * from loc1;
select * from rem1;
+
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %', tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+delete from rem1;
+insert into rem1 values(1,'insert');
+update rem1 set f2 = 'update' where f1 = 1;
+update rem1 set f2 = f2 || f2;
+
+
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+
+DELETE from rem1;
+
+
+-- Test WHEN conditions
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+
+
+-- Test various RETURN statements in BEFORE triggers.
+
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DELETE FROM rem1;
+
+-- Add a second trigger, to check that the changes are propagated corretly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+
+DELETE from rem1;
+
+INSERT INTO rem1 VALUES (1, 'test');
+
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+
+SELECT * from loc1;
+
+UPDATE rem1 SET f2 = 'test2';
+
+SELECT * from loc1;
+
+DELETE from rem1;
+
+SELECT * from loc1;
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1(f2) VALUES ('test');
+UPDATE rem1 SET f2 = 'testo';
+
+-- Test returning system attributes
+INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax;
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index e5ec738..07bc757 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -93,7 +93,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<para>
The following table summarizes which types of triggers may be used on
- tables and views:
+ tables, views and foreign tables:
</para>
<informaltable id="supported-trigger-types">
@@ -110,8 +110,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>BEFORE</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -121,8 +121,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>AFTER</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -244,7 +244,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
<term><replaceable class="parameter">table_name</replaceable></term>
<listitem>
<para>
- The name (optionally schema-qualified) of the table or view the trigger
+ The name (optionally schema-qualified) of the table, view or foreign table the trigger
is for.
</para>
</listitem>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index f579340..d9fc02d 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -33,11 +33,11 @@
<para>
A trigger is a specification that the database should automatically
execute a particular function whenever a certain type of operation is
- performed. Triggers can be attached to both tables and views.
+ performed. Triggers can be attached to tables, views and foreign tables.
</para>
<para>
- On tables, triggers can be defined to execute either before or after any
+ On tables and foreign tables, triggers can be defined to execute either before or after any
<command>INSERT</command>, <command>UPDATE</command>, or
<command>DELETE</command> operation, either once per modified row,
or once per <acronym>SQL</acronym> statement.
@@ -46,7 +46,7 @@
<command>UPDATE</command> statement.
Triggers can also fire for <command>TRUNCATE</command> statements.
If a trigger event occurs, the trigger's function is called at the
- appropriate time to handle the event.
+ appropriate time to handle the event. Foreign tables do not support the TRUNCATE statement at all.
</para>
<para>
@@ -108,15 +108,23 @@
statement starts to do anything, while statement-level <literal>AFTER</>
triggers fire at the very end of the statement. These types of
triggers may be defined on tables or views. Row-level <literal>BEFORE</>
- triggers fire immediately before a particular row is operated on,
- while row-level <literal>AFTER</> triggers fire at the end of the
- statement (but before any statement-level <literal>AFTER</> triggers).
- These types of triggers may only be defined on tables. Row-level
- <literal>INSTEAD OF</> triggers may only be defined on views, and fire
+ triggers fire immediately before a particular row is operated on.
+ For regular and foreign tables, row-level <literal>AFTER</> triggers fire at
+ the end of the statement (but before any statement-level <literal>AFTER</> triggers).
+ These types of triggers may only be defined on tables and foreign tables.
+ Row-level <literal>INSTEAD OF</> triggers may only be defined on views, and fire
immediately as each row in the view is identified as needing to be
operated on.
</para>
+ <caution>
+ <para>
+ On foreign tables, the number of modified tuples in foreign tables
+ with corresponding <literal>AFTER</> triggers cannot exceed <literal>INT_MAX</>
+ per transaction.
+ </para>
+ </caution>
+
<para>
Trigger functions invoked by per-statement triggers should always
return <symbol>NULL</symbol>. Trigger functions invoked by per-row
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 08b037e..32b976a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3175,6 +3175,9 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DisableTrig: /* DISABLE TRIGGER variants */
case AT_DisableTrigAll:
case AT_DisableTrigUser:
+ ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE);
+ pass = AT_PASS_MISC;
+ break;
case AT_EnableRule: /* ENABLE/DISABLE RULE variants */
case AT_EnableAlwaysRule:
case AT_EnableReplicaRule:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 86449a6..1b067c5 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -55,6 +55,7 @@
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/tqual.h"
+#include "utils/tuplestore.h"
/* GUC variables */
@@ -75,6 +76,9 @@ static HeapTuple GetTupleForTrigger(EState *estate,
ItemPointer tid,
LockTupleMode lockmode,
TupleTableSlot **newSlot);
+
+static HeapTuple ExtractOldTuple(TupleTableSlot *mixedtupleslot,
+ ResultRelInfo *relinfo);
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
Trigger *trigger, TriggerEvent event,
Bitmapset *modifiedCols,
@@ -184,12 +188,22 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
RelationGetRelationName(rel)),
errdetail("Views cannot have TRUNCATE triggers.")));
}
+ else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ 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("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
-
+ }
if (!allowSystemTableMods && IsSystemRelation(rel))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1062,10 +1076,11 @@ RemoveTriggerById(Oid trigOid)
rel = heap_open(relid, AccessExclusiveLock);
if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_VIEW)
+ rel->rd_rel->relkind != RELKIND_VIEW &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view",
+ errmsg("\"%s\" is not a table, view or foreign table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
@@ -1166,10 +1181,11 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
form = (Form_pg_class) GETSTRUCT(tuple);
/* only tables and views can have triggers */
- if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW)
+ if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW &&
+ form->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view", rv->relname)));
+ errmsg("\"%s\" is not a table, view or foreign table", rv->relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
@@ -1844,9 +1860,7 @@ ExecCallTriggerFunc(TriggerData *trigdata,
*/
InitFunctionCallInfoData(fcinfo, finfo, 0,
InvalidOid, (Node *) trigdata, NULL);
-
pgstat_init_function_usage(&fcinfo, &fcusage);
-
MyTriggerDepth++;
PG_TRY();
{
@@ -2155,9 +2169,18 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
HeapTuple newtuple;
TupleTableSlot *newSlot;
int i;
+ Relation rel = relinfo->ri_RelationDesc;
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- LockTupleExclusive, &newSlot);
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ LockTupleExclusive, &newSlot);
+ }
if (trigtuple == NULL)
return false;
@@ -2204,19 +2227,33 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
void
ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ TupleTableSlot *slot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since
+ * there cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ trigtuple = ExtractOldTuple(slot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ }
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
true, trigtuple, NULL, NIL, NULL);
heap_freetuple(trigtuple);
@@ -2335,7 +2372,8 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TupleTableSlot *
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid, TupleTableSlot *slot)
+ ItemPointer tupleid,
+ TupleTableSlot *slot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
HeapTuple slottuple = ExecMaterializeSlot(slot);
@@ -2345,6 +2383,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
HeapTuple oldtuple;
TupleTableSlot *newSlot;
int i;
+ Relation relation = relinfo->ri_RelationDesc;
Bitmapset *modifiedCols;
Bitmapset *keyCols;
LockTupleMode lockmode;
@@ -2355,7 +2394,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
* concurrency.
*/
modifiedCols = GetModifiedColumns(relinfo, estate);
- keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
+ keyCols = RelationGetIndexAttrBitmap(relation,
INDEX_ATTR_BITMAP_KEY);
if (bms_overlap(keyCols, modifiedCols))
lockmode = LockTupleExclusive;
@@ -2363,8 +2402,16 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
lockmode = LockTupleNoKeyExclusive;
/* get a copy of the on-disk tuple we are planning to update */
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- lockmode, &newSlot);
+ if (relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ lockmode, &newSlot);
+ }
if (trigtuple == NULL)
return NULL; /* cancel the update action */
@@ -2446,24 +2493,40 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid, HeapTuple newtuple,
+ ItemPointer tupleid,
+ HeapTuple newtuple,
+ TupleTableSlot *slot,
List *recheckIndexes)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since
+ * there cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ trigtuple = ExtractOldTuple(slot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ }
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
true, trigtuple, newtuple, recheckIndexes,
GetModifiedColumns(relinfo, estate));
heap_freetuple(trigtuple);
+
}
}
@@ -2731,6 +2794,32 @@ ltrmark:;
}
/*
+ * Get an old tuple from a "mixed tuple", containing both the new values as well
+ * as well as the old ones as resjunk columns.
+ */
+static HeapTuple
+ExtractOldTuple(TupleTableSlot *planSlot,
+ ResultRelInfo *relinfo)
+{
+ bool isNull;
+ JunkFilter *junkfilter = relinfo->ri_junkFilter;
+ HeapTuple oldtuple = palloc0(sizeof(HeapTupleData));
+ HeapTupleHeader td;
+ Datum datum = ExecGetJunkAttribute(planSlot,
+ junkfilter->jf_junkAttNo,
+ &isNull);
+
+ /* shouldn't ever get a null result... */
+ if (isNull)
+ elog(ERROR, "wholerow is NULL");
+ td = DatumGetHeapTupleHeader(datum);
+ oldtuple->t_len = HeapTupleHeaderGetDatumLength(td);
+ oldtuple->t_data = td;
+ oldtuple->t_tableOid = RelationGetRelid(relinfo->ri_RelationDesc);
+ return oldtuple;
+}
+
+/*
* Is trigger enabled to fire?
*/
static bool
@@ -2948,6 +3037,7 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_2CTIDS 0x10000000
#define AFTER_TRIGGER_DONE 0x20000000
#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
+#define AFTER_TRIGGER_FDW 0x80000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -2975,9 +3065,18 @@ typedef struct AfterTriggerEventDataOneCtid
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
} AfterTriggerEventDataOneCtid;
+typedef struct AfterTriggerEventDataFDW
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ int ate_ptr1; /* inserted, deleted, or old updated tuple */
+ int ate_ptr2; /* new updated tuple */
+} AfterTriggerEventDataFDW;
+
#define SizeofTriggerEvent(evt) \
(((evt)->ate_flags & AFTER_TRIGGER_2CTIDS) ? \
- sizeof(AfterTriggerEventData) : sizeof(AfterTriggerEventDataOneCtid))
+ sizeof(AfterTriggerEventData) : \
+ ((evt)->ate_flags & AFTER_TRIGGER_FDW) ? sizeof(AfterTriggerEventDataFDW) : \
+ sizeof(AfterTriggerEventDataOneCtid))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3071,6 +3170,9 @@ typedef struct AfterTriggerEventList
* each stack. (By not keeping our own stack pointer, we can avoid trouble
* in cases where errors during subxact abort cause multiple invocations
* of AfterTriggerEndSubXact() at the same nesting depth.)
+ *
+ * fdwtuplestore is used to store references for FDW tuples, since they cannot
+ * be retrieved normally.
*/
typedef struct AfterTriggersData
{
@@ -3089,6 +3191,9 @@ typedef struct AfterTriggersData
int *depth_stack; /* stacked query_depths */
CommandId *firing_stack; /* stacked firing_counters */
int maxtransdepth; /* allocated len of above arrays */
+ Tuplestorestate *fdwtuplestore;
+ int fdw_nextwrite;
+ int fdw_lastread;
} AfterTriggersData;
typedef AfterTriggersData *AfterTriggers;
@@ -3100,7 +3205,8 @@ static void AfterTriggerExecute(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
- MemoryContext per_tuple_context);
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot);
static SetConstraintState SetConstraintStateCreate(int numalloc);
static SetConstraintState SetConstraintStateCopy(SetConstraintState state);
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
@@ -3353,7 +3459,8 @@ static void
AfterTriggerExecute(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
- MemoryContext per_tuple_context)
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot)
{
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
@@ -3390,34 +3497,72 @@ AfterTriggerExecute(AfterTriggerEvent event,
/*
* Fetch the required tuple(s).
*/
- if (ItemPointerIsValid(&(event->ate_ctid1)))
+ if (event->ate_flags & AFTER_TRIGGER_FDW)
{
- ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) event;
+
+ if (afterTriggers->fdw_lastread > fdwevent->ate_ptr1)
+ {
+ tuplestore_rescan(afterTriggers->fdwtuplestore);
+ afterTriggers->fdw_lastread = 0;
+ }
+ while (afterTriggers->fdw_lastread < fdwevent->ate_ptr1)
+ {
+ tuplestore_advance(afterTriggers->fdwtuplestore, true);
+ afterTriggers->fdw_lastread++;
+ }
+ if (!tuplestore_gettupleslot(afterTriggers->fdwtuplestore, true, false, trig_tuple_slot))
+ {
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
- LocTriggerData.tg_trigtuple = &tuple1;
- LocTriggerData.tg_trigtuplebuf = buffer1;
- }
- else
- {
- LocTriggerData.tg_trigtuple = NULL;
+ }
+ afterTriggers->fdw_lastread++;
+ LocTriggerData.tg_trigtuple = ExecCopySlotTuple(trig_tuple_slot);
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
- }
- /* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) &&
- ItemPointerIsValid(&(event->ate_ctid2)))
- {
- ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
- elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
- LocTriggerData.tg_newtuple = &tuple2;
- LocTriggerData.tg_newtuplebuf = buffer2;
+ if (fdwevent->ate_ptr2 >= 0)
+ {
+ /* The new tuple necessarily follows the previous one */
+ if (!tuplestore_gettupleslot(afterTriggers->fdwtuplestore, true, false, trig_tuple_slot))
+ {
+ elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+ }
+ afterTriggers->fdw_lastread = afterTriggers->fdw_lastread + 1;
+ LocTriggerData.tg_newtuple = ExecCopySlotTuple(trig_tuple_slot);
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
+ tuplestore_select_read_pointer(afterTriggers->fdwtuplestore, 0);
}
else
{
- LocTriggerData.tg_newtuple = NULL;
- LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ if (ItemPointerIsValid(&(event->ate_ctid1)))
+ {
+ ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
+ elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+ LocTriggerData.tg_trigtuple = &tuple1;
+ LocTriggerData.tg_trigtuplebuf = buffer1;
+ }
+ else
+ {
+ LocTriggerData.tg_trigtuple = NULL;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ }
+
+ /* don't touch ctid2 if not there */
+ if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) &&
+ ItemPointerIsValid(&(event->ate_ctid2)))
+ {
+ ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
+ elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+ LocTriggerData.tg_newtuple = &tuple2;
+ LocTriggerData.tg_newtuplebuf = buffer2;
+ }
+ else
+ {
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
}
/*
@@ -3559,6 +3704,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
TriggerDesc *trigdesc = NULL;
FmgrInfo *finfo = NULL;
Instrumentation *instr = NULL;
+ TupleTableSlot *slot = NULL;
/* Make a local EState if need be */
if (estate == NULL)
@@ -3574,7 +3720,6 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
-
for_each_chunk(chunk, *events)
{
AfterTriggerEvent event;
@@ -3603,6 +3748,11 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
trigdesc = rInfo->ri_TrigDesc;
finfo = rInfo->ri_TrigFunctions;
instr = rInfo->ri_TrigInstrument;
+ /* Make a slot to read back tuple from the tuplestore */
+ if (slot != NULL)
+ ExecDropSingleTupleTableSlot(slot);
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ slot = MakeSingleTupleTableSlot(rel->rd_att);
if (trigdesc == NULL) /* should not happen */
elog(ERROR, "relation %u has no triggers",
evtshared->ats_relid);
@@ -3614,7 +3764,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
* won't try to re-fire it.
*/
AfterTriggerExecute(event, rel, trigdesc, finfo, instr,
- per_tuple_context);
+ per_tuple_context, slot);
/*
* Mark the event as done.
@@ -3645,6 +3795,9 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
events->tailfree = chunk->freeptr;
}
}
+ if (slot != NULL)
+ ExecDropSingleTupleTableSlot(slot);
+
/* Release working resources */
MemoryContextDelete(per_tuple_context);
@@ -3709,6 +3862,9 @@ AfterTriggerBeginXact(void)
afterTriggers->depth_stack = NULL;
afterTriggers->firing_stack = NULL;
afterTriggers->maxtransdepth = 0;
+ afterTriggers->fdwtuplestore = tuplestore_begin_heap(true, false, work_mem);
+ afterTriggers->fdw_nextwrite = 0;
+ afterTriggers->fdw_lastread = 0;
}
@@ -3751,6 +3907,7 @@ AfterTriggerBeginQuery(void)
events->head = NULL;
events->tail = NULL;
events->tailfree = NULL;
+
}
@@ -3900,8 +4057,14 @@ AfterTriggerEndXact(bool isCommit)
* of memory for the list!
*/
if (afterTriggers && afterTriggers->event_cxt)
+ {
MemoryContextDelete(afterTriggers->event_cxt);
-
+ }
+ if (afterTriggers && afterTriggers->fdwtuplestore)
+ {
+ tuplestore_end(afterTriggers->fdwtuplestore);
+ afterTriggers->fdwtuplestore = NULL;
+ }
afterTriggers = NULL;
}
@@ -4528,6 +4691,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
+ char relkind = relinfo->ri_RelationDesc->rd_rel->relkind;
int tgtype_event;
int tgtype_level;
int i;
@@ -4550,6 +4714,10 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* arrays.
*/
new_event.ate_flags = 0;
+ if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ {
+ new_event.ate_flags = AFTER_TRIGGER_FDW;
+ }
switch (event)
{
case TRIGGER_EVENT_INSERT:
@@ -4558,8 +4726,25 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup == NULL);
Assert(newtup != NULL);
- ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid1));
- ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) & new_event;
+
+ if (afterTriggers->fdw_nextwrite == INT_MAX)
+ {
+ elog(ERROR, "Cannot insert, update or delete more than %i tuples in "
+ "foreign tables with AFTER row-level trigger", INT_MAX);
+ }
+ tuplestore_puttuple(afterTriggers->fdwtuplestore, newtup);
+ fdwevent->ate_ptr1 = afterTriggers->fdw_nextwrite;
+ afterTriggers->fdw_nextwrite++;
+ fdwevent->ate_ptr2 = -1;
+ }
+ else
+ {
+ ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ }
}
else
{
@@ -4575,8 +4760,26 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup != NULL);
Assert(newtup == NULL);
- ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
- ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) & new_event;
+
+ if (afterTriggers->fdw_nextwrite == INT_MAX)
+ {
+ elog(ERROR, "Cannot insert, update or delete more than %i tuples in "
+ "foreign tables with AFTER row-level trigger", INT_MAX);
+ }
+ tuplestore_puttuple(afterTriggers->fdwtuplestore, oldtup);
+ fdwevent->ate_ptr1 = afterTriggers->fdw_nextwrite;
+ afterTriggers->fdw_nextwrite++;
+
+ fdwevent->ate_ptr2 = -1;
+ }
+ else
+ {
+ ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ }
}
else
{
@@ -4592,9 +4795,29 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup != NULL);
Assert(newtup != NULL);
- ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
- ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
- new_event.ate_flags |= AFTER_TRIGGER_2CTIDS;
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) & new_event;
+
+ if (afterTriggers->fdw_nextwrite >= INT_MAX - 1)
+ {
+ elog(ERROR, "Cannot insert, update or delete more than %i tuples in "
+ "foreign tables with AFTER row-level trigger", INT_MAX);
+ }
+ tuplestore_puttuple(afterTriggers->fdwtuplestore, oldtup);
+ fdwevent->ate_ptr1 = afterTriggers->fdw_nextwrite;
+ afterTriggers->fdw_nextwrite++;
+ tuplestore_puttuple(afterTriggers->fdwtuplestore, newtup);
+ fdwevent->ate_ptr2 = afterTriggers->fdw_nextwrite;
+ afterTriggers->fdw_nextwrite++;
+
+ }
+ else
+ {
+ ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
+ new_event.ate_flags |= AFTER_TRIGGER_2CTIDS;
+ }
}
else
{
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 6f0f47e..4553f4e 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -488,7 +488,7 @@ ldelete:;
(estate->es_processed)++;
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, planSlot);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
@@ -788,7 +788,7 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
+ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, planSlot,
recheckIndexes);
list_free(recheckIndexes);
@@ -986,7 +986,18 @@ ExecModifyTable(ModifyTableState *node)
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
- /* do nothing; FDW must fetch any junk attrs it wants */
+ /*
+ * If the junkAttNo is valid, then it identifies the
+ * wholerow attribute. This is the case when there is an
+ * UPDATE or DELETE trigger.
+ */
+ if (AttributeNumberIsValid(junkfilter->jf_junkAttNo))
+ {
+ datum = ExecGetJunkAttribute(slot,
+ junkfilter->jf_junkAttNo,
+ &isNull);
+ oldtuple = DatumGetHeapTupleHeader(datum);
+ }
}
else
{
@@ -1334,7 +1345,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
- /* FDW must fetch any junk attrs it wants */
+ /*
+ * FDW must fetch any junk attrs it wants When there
+ * is an AFTER trigger, there should be a wholerow
+ * attribute.
+ */
+ AttrNumber junkAttNo = ExecFindJunkAttribute(j, "wholerow");
+
+ if (AttributeNumberIsValid(junkAttNo))
+ j->jf_junkAttNo = junkAttNo;
}
else
{
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 35bda67..52cc5b7 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -240,7 +240,22 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->commandType = parse->commandType;
result->queryId = parse->queryId;
- result->hasReturning = (parse->returningList != NIL);
+
+ /*
+ * Mark the result as having RETURNING only if the returning target list
+ * has non-resjunk entries
+ */
+ result->hasReturning = false;
+ foreach(lp, parse->returningList)
+ {
+ TargetEntry *tle = lfirst(lp);
+
+ if (!tle->resjunk)
+ {
+ result->hasReturning = true;
+ break;
+ }
+ }
result->hasModifyingCTE = parse->hasModifyingCTE;
result->canSetTag = parse->canSetTag;
result->transientPlan = glob->transientPlan;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 0b13645..6489548 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -45,7 +45,8 @@ static Query *rewriteRuleAction(Query *parsetree,
CmdType event,
bool *returning_flag);
static List *adjustJoinTreeList(Query *parsetree, bool removert, int rt_index);
-static void rewriteTargetListIU(Query *parsetree, Relation target_relation,
+static void rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte,
+ Relation target_relation,
List **attrno_list);
static TargetEntry *process_matched_tle(TargetEntry *src_tle,
TargetEntry *prior_tle,
@@ -642,6 +643,10 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
* 4. Sort the tlist into standard order: non-junk fields in order by resno,
* then junk fields (these in no particular order).
*
+ * 5. For an insert on a foreign table with an after trigger, add missing
+ * attribute to the returning targetlist. This is needed to ensure that all
+ * attributes are fetched from the remote side on a returning statement.
+ *
* We must do items 1,2,3 before firing rewrite rules, else rewritten
* references to NEW.foo will produce wrong or incomplete results. Item 4
* is not needed for rewriting, but will be needed by the planner, and we
@@ -653,8 +658,8 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
* processing VALUES RTEs.
*/
static void
-rewriteTargetListIU(Query *parsetree, Relation target_relation,
- List **attrno_list)
+rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte,
+ Relation target_relation, List **attrno_list)
{
CmdType commandType = parsetree->commandType;
TargetEntry **new_tles;
@@ -730,6 +735,28 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
}
}
+ /*
+ * For foreign tables, force RETURNING the whole-row if a corresponding
+ * AFTER trigger is found
+ */
+ if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE &&
+ target_relation->trigdesc &&
+ ((commandType == CMD_INSERT && target_relation->trigdesc->trig_insert_after_row) ||
+ (commandType == CMD_UPDATE && target_relation->trigdesc->trig_update_after_row)))
+
+ {
+ Var *var = makeWholeRowVar(target_rte,
+ parsetree->resultRelation,
+ 0,
+ false);
+ TargetEntry *tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->returningList) + 1,
+ "wholerow",
+ true);
+
+ parsetree->returningList = lappend(parsetree->returningList, tle);
+ }
+
for (attrno = 1; attrno <= numattrs; attrno++)
{
TargetEntry *new_tle = new_tles[attrno - 1];
@@ -818,7 +845,6 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
}
pfree(new_tles);
-
parsetree->targetList = list_concat(new_tlist, junk_tlist);
}
@@ -1174,7 +1200,7 @@ static void
rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
Relation target_relation)
{
- Var *var;
+ Var *var = NULL;
const char *attrname;
TargetEntry *tle;
@@ -1206,7 +1232,27 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
target_relation);
- return;
+ /*
+ * If we have a trigger corresponding to the operation, add a wholerow
+ * attribute.
+ */
+ if (target_relation->trigdesc &&
+ ((parsetree->commandType == CMD_UPDATE &&
+ (target_relation->trigdesc->trig_update_after_row
+ || target_relation->trigdesc->trig_update_before_row)) ||
+ (parsetree->commandType == CMD_DELETE &&
+ (target_relation->trigdesc->trig_delete_after_row ||
+ target_relation->trigdesc->trig_delete_before_row))))
+ {
+ var = makeWholeRowVar(target_rte,
+ parsetree->resultRelation,
+ 0,
+ false);
+
+ attrname = "wholerow";
+
+ }
+
}
else
{
@@ -1221,13 +1267,15 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
attrname = "wholerow";
}
+ if (var != NULL)
+ {
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->targetList) + 1,
+ pstrdup(attrname),
+ true);
- tle = makeTargetEntry((Expr *) var,
- list_length(parsetree->targetList) + 1,
- pstrdup(attrname),
- true);
-
- parsetree->targetList = lappend(parsetree->targetList, tle);
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+ }
}
@@ -2965,19 +3013,19 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
List *attrnos;
/* Process the main targetlist ... */
- rewriteTargetListIU(parsetree, rt_entry_relation, &attrnos);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, &attrnos);
/* ... and the VALUES expression lists */
rewriteValuesRTE(values_rte, rt_entry_relation, attrnos);
}
else
{
/* Process just the main targetlist */
- rewriteTargetListIU(parsetree, rt_entry_relation, NULL);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL);
}
}
else if (event == CMD_UPDATE)
{
- rewriteTargetListIU(parsetree, rt_entry_relation, NULL);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL);
rewriteTargetListUD(parsetree, rt_entry, rt_entry_relation);
}
else if (event == CMD_DELETE)
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 44d686c..ed598f0 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -150,7 +150,8 @@ extern bool ExecBRDeleteTriggers(EState *estate,
ItemPointer tupleid);
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ TupleTableSlot *slot);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -167,6 +168,7 @@ extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple newtuple,
+ TupleTableSlot *slot,
List *recheckIndexes);
extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 60506e0..3405b6c 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -1158,6 +1158,36 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
ERROR: must be owner of foreign server s9
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+ERROR: "foreign_table_1" is a foreign table
+DETAIL: Foreign Tables cannot have constraint triggers.
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+DROP FUNCTION dummy_trigger();
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
ERROR: foreign table "no_table" does not exist
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index f819eb1..6166097 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -470,6 +470,43 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+
+DROP FUNCTION dummy_trigger();
+
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
DROP FOREIGN TABLE IF EXISTS no_table;
On Thu, Jan 23, 2014 at 03:17:35PM +0100, Ronan Dunklau wrote:
- for after triggers, the whole queuing mechanism is bypassed for foreign
tables. This is IMO acceptable, since foreign tables cannot have
constraints or constraints triggers, and thus have not need for
deferrable execution. This design avoids the need for storing and
retrieving/identifying remote tuples until the query or transaction end.Whether an AFTER ROW trigger is deferred determines whether it runs at the
end of the firing query or at the end of the firing query's transaction.
In all cases, every BEFORE ROW trigger of a given query fires before any
AFTER ROW trigger of the same query. SQL requires that. This proposal
would give foreign table AFTER ROW triggers a novel firing time; let's not
do that.I think the options going forward are either (a) design a way to queue
foreign table AFTER ROW triggers such that we can get the old and/or new
rows at the end of the query or (b) not support AFTER ROW triggers on
foreign tables for the time being.I did not know this was mandated by the standard.
The attached patch tries to solve this problem by allocating a tuplestore
in the global afterTriggers structure. This tuplestore is used for the whole
transaction, and uses only work_mem per transaction.Both old and new tuples are stored in this tuplestore. Some additional
bookkeeping is done on the afterTriggers global structure, to keep track of
the number of inserted tuples, and the current read pointer position. The
tuples are identified by their order of insertion during the transaction.
I think this could benefit from some support in the tuplestore API, by
allowing arbitrary seek without the need to store more ReadPointers.I initially tried to keep track of them by allocating read pointers on the
tuple store, but it turned out to be so expensive that I had to find another
way (24bytes per stored tuple, which are not reclaimable until the end of the
transaction).What do you think about this approach ? Is there something I missed which
would make it not sustainable ?
Seems basically reasonable. I foresee multiple advantages from having one
tuplestore per query level as opposed to one for the entire transaction. You
would remove the performance trap of backing up the tuplestore by rescanning.
It permits reclaiming memory and disk space in AfterTriggerEndQuery() rather
than at end of transaction. You could remove ate_ptr1 and ate_ptr2 from
AfterTriggerEventDataFDW and just store the flags word: depending on
AFTER_TRIGGER_2CTIDS, grab either the next one or the next two tuples from the
tuplestore. Using work_mem per AfterTriggerBeginQuery() instead of per
transaction is no problem. What do you think of that design change?
If you do pursue that change, make sure the code still does the right thing
when it drops queued entries during subxact abort.
I do not have access to the standard specification, any advice regarding
specs compliance would be welcomed.
http://wiki.postgresql.org/wiki/Developer_FAQ#Where_can_I_get_a_copy_of_the_SQL_standards.3F
There is still one small issue with the attached patch: modifications to
the tuple performed by the foreign data wrapper (via the returned
TupleTableSlot in ExecForeignUpdate and ExecForeignInsert hooks) are not
visible to the AFTER trigger. This could be fixed by merging the planslot
containing the resjunk columns with the returned slot before calling the
trigger, but I'm not really sure how to safely perform that. Any advice ?Currently, FDWs are permitted to skip returning columns not actually
referenced by any RETURNING clause. I would change that part of the API
contract to require returning all columns when an AFTER ROW trigger is
involved. You can't get around doing that by merging old column values,
because, among other reasons, an INSERT does not have those values at all.I'm not sure this should be part of the API contract: it would make
implementing a FDW more complicated than it is now. The attached patch hooks
on rewriteTargetListIU to add the missing targets to the returning clause,
when needed.
You're effectively faking the presence of a RETURNING list so today's
conforming FDWs will already do the right thing? Clever.
Please don't change unrelated whitespace.
Please use pgindent to fix the formatting of your new code. It's fine to
introduce occasional whitespace errors, but they're unusually-plentiful
here.I think its done now. One problem I have with running pgindent is that I
accidentally add chunks that were modified only by pgindent.
Yep; that is a pain.
Note that pgindent can't fix many unrelated whitespace changes. For example,
if you add or remove a blank line, pgindent won't interfere. (We would not
want it to interfere, because the use of blank lines is up to the code
author.) You will still need to read through your diff for such things.
On Wed, Jan 29, 2014 at 12:44:16PM +0100, Ronan Dunklau wrote:
Le mercredi 29 janvier 2014 09:13:36 Kouhei Kaigai a �crit :
It may make sense to put a check fdw_nextwrite is less than INT_MAX. :-)
The attached patch checks this, and add documentation for this limitation.
I'm not really sure about how to phrase that correctly in the error message
and the documentation. One can store at most INT_MAX foreign tuples, which
means that at most INT_MAX insert or delete or "half-updates" can occur. By
half-updates, I mean that for update two tuples are stored whereas in contrast
to only one for insert and delete trigger.Besides, I don't know where this disclaimer should be in the documentation.
Any advice here ?
I wouldn't mention that limitation.
@@ -3390,34 +3497,72 @@ AfterTriggerExecute(AfterTriggerEvent event, /* * Fetch the required tuple(s). */ - if (ItemPointerIsValid(&(event->ate_ctid1))) + if (event->ate_flags & AFTER_TRIGGER_FDW) { - ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self)); - if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL)) + AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) event; + + if (afterTriggers->fdw_lastread > fdwevent->ate_ptr1) + { + tuplestore_rescan(afterTriggers->fdwtuplestore); + afterTriggers->fdw_lastread = 0; + } + while (afterTriggers->fdw_lastread < fdwevent->ate_ptr1) + { + tuplestore_advance(afterTriggers->fdwtuplestore, true); + afterTriggers->fdw_lastread++; + }
This is the performance trap I mentioned above; it brings potential O(n^2)
complexity to certain AFTER trigger execution scenarios.
@@ -3709,6 +3862,9 @@ AfterTriggerBeginXact(void) afterTriggers->depth_stack = NULL; afterTriggers->firing_stack = NULL; afterTriggers->maxtransdepth = 0; + afterTriggers->fdwtuplestore = tuplestore_begin_heap(true, false, work_mem);
Probably best to create the tuplestore lazily, similar to how we initialize
afterTriggers->event_cxt. tuplestore_begin_heap() is almost cheap enough to
call unconditionally, but relatively few queries will use this.
@@ -986,7 +986,18 @@ ExecModifyTable(ModifyTableState *node) } else if (relkind == RELKIND_FOREIGN_TABLE) { - /* do nothing; FDW must fetch any junk attrs it wants */ + /* + * If the junkAttNo is valid, then it identifies the + * wholerow attribute. This is the case when there is an + * UPDATE or DELETE trigger. + */ + if (AttributeNumberIsValid(junkfilter->jf_junkAttNo)) + { + datum = ExecGetJunkAttribute(slot, + junkfilter->jf_junkAttNo, + &isNull); + oldtuple = DatumGetHeapTupleHeader(datum);
Check "isNull", just in case. See similar code elsewhere in this file.
+ } } else { @@ -1334,7 +1345,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) } else if (relkind == RELKIND_FOREIGN_TABLE) { - /* FDW must fetch any junk attrs it wants */ + /* + * FDW must fetch any junk attrs it wants When there + * is an AFTER trigger, there should be a wholerow + * attribute.
This comment edit looks half-done.
--
Noah Misch
EnterpriseDB http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Thank you for this new review, please find attached a revised patch.
Le jeudi 30 janvier 2014 14:05:08 Noah Misch a écrit :
On Thu, Jan 23, 2014 at 03:17:35PM +0100, Ronan Dunklau wrote:
What do you think about this approach ? Is there something I missed which
would make it not sustainable ?Seems basically reasonable. I foresee multiple advantages from having one
tuplestore per query level as opposed to one for the entire transaction.
You would remove the performance trap of backing up the tuplestore by
rescanning. It permits reclaiming memory and disk space in
AfterTriggerEndQuery() rather than at end of transaction. You could remove
ate_ptr1 and ate_ptr2 from AfterTriggerEventDataFDW and just store the
flags word: depending on AFTER_TRIGGER_2CTIDS, grab either the next one or
the next two tuples from the tuplestore. Using work_mem per
AfterTriggerBeginQuery() instead of per transaction is no problem. What do
you think of that design change?
I agree that this design is better, but I have some objections.
We can remove ate_ptr2 and rely on the AFTER_TRIGGER_2CTIDS flag, but the
rescanning and ate_ptr1 (renamed ate_tupleindex in the attached patch) can't
go away.
Consider for example the case of a foreign table with more than one AFTER
UPDATE triggers. Unless we store the tuples once for each trigger, we will
have to rescan the tuplestore.
To mitigate the effects of this behaviour, I added the option to perform a
reverse_seek when the looked-up tuple is nearer from the current index than
from the start.
Probably best to create the tuplestore lazily, similar to how we initialize
afterTriggers->event_cxt. tuplestore_begin_heap() is almost cheap enough to
call unconditionally, but relatively few queries will use this.
The per-query tuplestore design stores one pointer per FDWTuplestore struct.
This struct is only used to keep track of the current READ and WRITE pointer
positions along with the tuplestore itself.
The actual structure will be initialized only if the query needs it.
If you do pursue that change, make sure the code still does the right thing
when it drops queued entries during subxact abort.
I don't really understand what should be done at that stage. Since triggers on
foreign tables are not allowed to be deferred, everything should be cleaned up
at the end of each query, right ? So, there shouldn't be any queued entries.
I do not have access to the standard specification, any advice regarding
specs compliance would be welcomed.http://wiki.postgresql.org/wiki/Developer_FAQ#Where_can_I_get_a_copy_of_the_
SQL_standards.3F
Thank you. I did not find anything more than what you noted. I think that even
if the bit about foreign tables being mentioned from a column list, and the
fact that a DROP TABLE should not create a new trigger execution context are
confusing, the fact that the CREATE TRIGGER definition explicitly mentions
either base tables or views makes me think that foreign tables are not
considered.
You're effectively faking the presence of a RETURNING list so today's
conforming FDWs will already do the right thing? Clever.
Yes, that's it. I hope I didn't introduce any side effects with regards to the
meaning of the hasReturning flag.
Note that pgindent can't fix many unrelated whitespace changes. For
example, if you add or remove a blank line, pgindent won't interfere. (We
would not want it to interfere, because the use of blank lines is up to the
code author.) You will still need to read through your diff for such
things.
Ok.
The attached patch checks this, and add documentation for this limitation.
I'm not really sure about how to phrase that correctly in the error
message
and the documentation. One can store at most INT_MAX foreign tuples, which
means that at most INT_MAX insert or delete or "half-updates" can occur.
By
half-updates, I mean that for update two tuples are stored whereas in
contrast to only one for insert and delete trigger.Besides, I don't know where this disclaimer should be in the
documentation.
Any advice here ?I wouldn't mention that limitation.
Maybe it shouldn't be so prominent, but I still think a note somewhere
couldn't hurt.
Should the use of work_mem be documented somewhere, too ?
This is the performance trap I mentioned above; it brings potential O(n^2)
complexity to certain AFTER trigger execution scenarios.
What scenarios do you have in mind ? I "fixed" the problem when there are
multiple triggers on a foreign table, do you have any other one ?
Check "isNull", just in case. See similar code elsewhere in this file.
Ok.
This comment edit looks half-done.
Ok.
--
Ronan Dunklau
http://dalibo.com - http://dalibo.org
Attachments:
foreign_trigger_v7.patchtext/x-patch; charset=UTF-8; name=foreign_trigger_v7.patchDownload
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 38c6cf8..f77144f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2372,3 +2372,318 @@ select * from rem1;
11 | bye remote
(4 rows)
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %', tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+delete from rem1;
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = AFTER, level = STATEMENT
+insert into rem1 values(1,'insert');
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = AFTER, level = STATEMENT
+update rem1 set f2 = 'update' where f1 = 1;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+update rem1 set f2 = f2 || f2;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+DELETE from rem1;
+-- Test WHEN conditions
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+NOTICE: trig_row_before_delete(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+NOTICE: trig_row_after_delete(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+-- Test various RETURN statements in BEFORE triggers.
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------
+ insert triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+ 2 | insert triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------
+ 1 | triggered !
+ 2 | triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------
+ skidoo triggered !
+ skidoo triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | skidoo triggered !
+ 2 | skidoo triggered !
+(2 rows)
+
+DELETE FROM rem1;
+-- Add a second trigger, to check that the changes are propagated corretly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------------------
+ insert triggered ! triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+ 2 | insert triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------
+ 1 | triggered ! triggered !
+ 2 | triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------------------
+ skidoo triggered ! triggered !
+ skidoo triggered ! triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | skidoo triggered ! triggered !
+ 2 | skidoo triggered ! triggered !
+(2 rows)
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+DELETE from rem1;
+INSERT INTO rem1 VALUES (1, 'test');
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+UPDATE rem1 SET f2 = 'test2';
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DELETE from rem1;
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1(f2) VALUES ('test');
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (12,test)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (12,"test triggered !")
+UPDATE rem1 SET f2 = 'testo';
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,testo)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,"testo triggered !")
+-- Test returning system attributes
+INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax;
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (13,test)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (13,"test triggered !")
+ ctid | xmin | xmax
+--------+------+------------
+ (0,27) | 180 | 4294967295
+(1 row)
+
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index ce8bb75..54b35d9 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -384,3 +384,215 @@ insert into loc1(f2) values('bye');
insert into rem1(f2) values('bye remote');
select * from loc1;
select * from rem1;
+
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %', tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+delete from rem1;
+insert into rem1 values(1,'insert');
+update rem1 set f2 = 'update' where f1 = 1;
+update rem1 set f2 = f2 || f2;
+
+
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+
+DELETE from rem1;
+
+
+-- Test WHEN conditions
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+
+
+-- Test various RETURN statements in BEFORE triggers.
+
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DELETE FROM rem1;
+
+-- Add a second trigger, to check that the changes are propagated corretly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+
+DELETE from rem1;
+
+INSERT INTO rem1 VALUES (1, 'test');
+
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+
+SELECT * from loc1;
+
+UPDATE rem1 SET f2 = 'test2';
+
+SELECT * from loc1;
+
+DELETE from rem1;
+
+SELECT * from loc1;
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1(f2) VALUES ('test');
+UPDATE rem1 SET f2 = 'testo';
+
+-- Test returning system attributes
+INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax;
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index e5ec738..07bc757 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -93,7 +93,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<para>
The following table summarizes which types of triggers may be used on
- tables and views:
+ tables, views and foreign tables:
</para>
<informaltable id="supported-trigger-types">
@@ -110,8 +110,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>BEFORE</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -121,8 +121,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>AFTER</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -244,7 +244,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
<term><replaceable class="parameter">table_name</replaceable></term>
<listitem>
<para>
- The name (optionally schema-qualified) of the table or view the trigger
+ The name (optionally schema-qualified) of the table, view or foreign table the trigger
is for.
</para>
</listitem>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index f579340..d9fc02d 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -33,11 +33,11 @@
<para>
A trigger is a specification that the database should automatically
execute a particular function whenever a certain type of operation is
- performed. Triggers can be attached to both tables and views.
+ performed. Triggers can be attached to tables, views and foreign tables.
</para>
<para>
- On tables, triggers can be defined to execute either before or after any
+ On tables and foreign tables, triggers can be defined to execute either before or after any
<command>INSERT</command>, <command>UPDATE</command>, or
<command>DELETE</command> operation, either once per modified row,
or once per <acronym>SQL</acronym> statement.
@@ -46,7 +46,7 @@
<command>UPDATE</command> statement.
Triggers can also fire for <command>TRUNCATE</command> statements.
If a trigger event occurs, the trigger's function is called at the
- appropriate time to handle the event.
+ appropriate time to handle the event. Foreign tables do not support the TRUNCATE statement at all.
</para>
<para>
@@ -108,15 +108,23 @@
statement starts to do anything, while statement-level <literal>AFTER</>
triggers fire at the very end of the statement. These types of
triggers may be defined on tables or views. Row-level <literal>BEFORE</>
- triggers fire immediately before a particular row is operated on,
- while row-level <literal>AFTER</> triggers fire at the end of the
- statement (but before any statement-level <literal>AFTER</> triggers).
- These types of triggers may only be defined on tables. Row-level
- <literal>INSTEAD OF</> triggers may only be defined on views, and fire
+ triggers fire immediately before a particular row is operated on.
+ For regular and foreign tables, row-level <literal>AFTER</> triggers fire at
+ the end of the statement (but before any statement-level <literal>AFTER</> triggers).
+ These types of triggers may only be defined on tables and foreign tables.
+ Row-level <literal>INSTEAD OF</> triggers may only be defined on views, and fire
immediately as each row in the view is identified as needing to be
operated on.
</para>
+ <caution>
+ <para>
+ On foreign tables, the number of modified tuples in foreign tables
+ with corresponding <literal>AFTER</> triggers cannot exceed <literal>INT_MAX</>
+ per transaction.
+ </para>
+ </caution>
+
<para>
Trigger functions invoked by per-statement triggers should always
return <symbol>NULL</symbol>. Trigger functions invoked by per-row
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 08b037e..32b976a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3175,6 +3175,9 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DisableTrig: /* DISABLE TRIGGER variants */
case AT_DisableTrigAll:
case AT_DisableTrigUser:
+ ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE);
+ pass = AT_PASS_MISC;
+ break;
case AT_EnableRule: /* ENABLE/DISABLE RULE variants */
case AT_EnableAlwaysRule:
case AT_EnableReplicaRule:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 86449a6..a910fac 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -55,6 +55,7 @@
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/tqual.h"
+#include "utils/tuplestore.h"
/* GUC variables */
@@ -75,6 +76,9 @@ static HeapTuple GetTupleForTrigger(EState *estate,
ItemPointer tid,
LockTupleMode lockmode,
TupleTableSlot **newSlot);
+
+static HeapTuple ExtractOldTuple(TupleTableSlot *mixedtupleslot,
+ ResultRelInfo *relinfo);
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
Trigger *trigger, TriggerEvent event,
Bitmapset *modifiedCols,
@@ -184,12 +188,22 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
RelationGetRelationName(rel)),
errdetail("Views cannot have TRUNCATE triggers.")));
}
+ else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ 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("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
-
+ }
if (!allowSystemTableMods && IsSystemRelation(rel))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1062,10 +1076,11 @@ RemoveTriggerById(Oid trigOid)
rel = heap_open(relid, AccessExclusiveLock);
if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_VIEW)
+ rel->rd_rel->relkind != RELKIND_VIEW &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view",
+ errmsg("\"%s\" is not a table, view or foreign table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
@@ -1166,10 +1181,11 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
form = (Form_pg_class) GETSTRUCT(tuple);
/* only tables and views can have triggers */
- if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW)
+ if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW &&
+ form->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view", rv->relname)));
+ errmsg("\"%s\" is not a table, view or foreign table", rv->relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
@@ -1844,9 +1860,7 @@ ExecCallTriggerFunc(TriggerData *trigdata,
*/
InitFunctionCallInfoData(fcinfo, finfo, 0,
InvalidOid, (Node *) trigdata, NULL);
-
pgstat_init_function_usage(&fcinfo, &fcusage);
-
MyTriggerDepth++;
PG_TRY();
{
@@ -2155,9 +2169,18 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
HeapTuple newtuple;
TupleTableSlot *newSlot;
int i;
+ Relation rel = relinfo->ri_RelationDesc;
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- LockTupleExclusive, &newSlot);
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ LockTupleExclusive, &newSlot);
+ }
if (trigtuple == NULL)
return false;
@@ -2204,19 +2227,33 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
void
ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ TupleTableSlot *slot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since
+ * there cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ trigtuple = ExtractOldTuple(slot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ }
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
true, trigtuple, NULL, NIL, NULL);
heap_freetuple(trigtuple);
@@ -2335,7 +2372,8 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TupleTableSlot *
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid, TupleTableSlot *slot)
+ ItemPointer tupleid,
+ TupleTableSlot *slot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
HeapTuple slottuple = ExecMaterializeSlot(slot);
@@ -2345,6 +2383,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
HeapTuple oldtuple;
TupleTableSlot *newSlot;
int i;
+ Relation relation = relinfo->ri_RelationDesc;
Bitmapset *modifiedCols;
Bitmapset *keyCols;
LockTupleMode lockmode;
@@ -2355,7 +2394,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
* concurrency.
*/
modifiedCols = GetModifiedColumns(relinfo, estate);
- keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
+ keyCols = RelationGetIndexAttrBitmap(relation,
INDEX_ATTR_BITMAP_KEY);
if (bms_overlap(keyCols, modifiedCols))
lockmode = LockTupleExclusive;
@@ -2363,8 +2402,16 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
lockmode = LockTupleNoKeyExclusive;
/* get a copy of the on-disk tuple we are planning to update */
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- lockmode, &newSlot);
+ if (relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ lockmode, &newSlot);
+ }
if (trigtuple == NULL)
return NULL; /* cancel the update action */
@@ -2446,24 +2493,40 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid, HeapTuple newtuple,
+ ItemPointer tupleid,
+ HeapTuple newtuple,
+ TupleTableSlot *slot,
List *recheckIndexes)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since
+ * there cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ trigtuple = ExtractOldTuple(slot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ }
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
true, trigtuple, newtuple, recheckIndexes,
GetModifiedColumns(relinfo, estate));
heap_freetuple(trigtuple);
+
}
}
@@ -2731,6 +2794,33 @@ ltrmark:;
}
/*
+ * Get an old tuple from a "mixed tuple", containing both the new values as well
+ * as well as the old ones as resjunk columns.
+ */
+static HeapTuple
+ExtractOldTuple(TupleTableSlot *planSlot,
+ ResultRelInfo *relinfo)
+{
+ bool isNull;
+ JunkFilter *junkfilter = relinfo->ri_junkFilter;
+ HeapTuple oldtuple = palloc0(sizeof(HeapTupleData));
+ HeapTupleHeader td;
+ Datum datum = ExecGetJunkAttribute(planSlot,
+ junkfilter->jf_junkAttNo,
+ &isNull);
+
+ /* shouldn't ever get a null result... */
+ if (isNull)
+ elog(ERROR, "wholerow is NULL");
+ td = DatumGetHeapTupleHeader(datum);
+ oldtuple->t_len = HeapTupleHeaderGetDatumLength(td);
+ oldtuple->t_data = td;
+ oldtuple->t_tableOid = RelationGetRelid(relinfo->ri_RelationDesc);
+ return oldtuple;
+}
+
+
+/*
* Is trigger enabled to fire?
*/
static bool
@@ -2948,6 +3038,7 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_2CTIDS 0x10000000
#define AFTER_TRIGGER_DONE 0x20000000
#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
+#define AFTER_TRIGGER_FDW 0x80000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -2975,9 +3066,17 @@ typedef struct AfterTriggerEventDataOneCtid
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
} AfterTriggerEventDataOneCtid;
+typedef struct AfterTriggerEventDataFDW
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ int ate_tupleindex; /* index of the first tuple */
+} AfterTriggerEventDataFDW;
+
#define SizeofTriggerEvent(evt) \
(((evt)->ate_flags & AFTER_TRIGGER_2CTIDS) ? \
- sizeof(AfterTriggerEventData) : sizeof(AfterTriggerEventDataOneCtid))
+ sizeof(AfterTriggerEventData) : \
+ ((evt)->ate_flags & AFTER_TRIGGER_FDW) ? sizeof(AfterTriggerEventDataFDW) : \
+ sizeof(AfterTriggerEventDataOneCtid))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3020,6 +3119,14 @@ typedef struct AfterTriggerEventList
for_each_chunk(cptr, evtlist) for_each_event(eptr, cptr)
+/* A wrapper around Tuplestorestate to keep the last read/written positions */
+typedef struct FDWTuplestore
+{
+ Tuplestorestate *fdwts_tuplestore;
+ int fdwts_nextwrite;
+ int fdwts_lastread;
+} FDWTuplestore;
+
/*
* All per-transaction data for the AFTER TRIGGERS module.
*
@@ -3050,7 +3157,11 @@ typedef struct AfterTriggerEventList
* immediate-mode triggers, and append any deferred events to the main events
* list.
*
- * maxquerydepth is just the allocated length of query_stack.
+ * fdw_tuplestores[query_depth] is a list of FDWTuplestore containing the
+ * foreign tuples needed for the current query.
+ *
+ * maxquerydepth is just the allocated length of query_stack and
+ * fdw_tuplestores.
*
* state_stack is a stack of pointers to saved copies of the SET CONSTRAINTS
* state data; each subtransaction level that modifies that state first
@@ -3071,6 +3182,7 @@ typedef struct AfterTriggerEventList
* each stack. (By not keeping our own stack pointer, we can avoid trouble
* in cases where errors during subxact abort cause multiple invocations
* of AfterTriggerEndSubXact() at the same nesting depth.)
+ *
*/
typedef struct AfterTriggersData
{
@@ -3089,24 +3201,45 @@ typedef struct AfterTriggersData
int *depth_stack; /* stacked query_depths */
CommandId *firing_stack; /* stacked firing_counters */
int maxtransdepth; /* allocated len of above arrays */
+ FDWTuplestore **fdw_tuplestores; /* tuplestore for each query */
} AfterTriggersData;
typedef AfterTriggersData *AfterTriggers;
static AfterTriggers afterTriggers;
-
static void AfterTriggerExecute(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
- MemoryContext per_tuple_context);
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot);
static SetConstraintState SetConstraintStateCreate(int numalloc);
static SetConstraintState SetConstraintStateCopy(SetConstraintState state);
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
Oid tgoid, bool tgisdeferred);
+/*
+ * Gets the current query fdwtuple store, and inializes it
+ * if necessary
+ */
+static FDWTuplestore *
+GetCurrentFDWTuplestore()
+{
+ FDWTuplestore *fdw_tuplestore = afterTriggers->fdw_tuplestores[afterTriggers->query_depth];
+
+ if (fdw_tuplestore == NULL)
+ {
+ fdw_tuplestore = palloc(sizeof(FDWTuplestore));
+ fdw_tuplestore->fdwts_tuplestore = tuplestore_begin_heap(true, false, work_mem);
+ fdw_tuplestore->fdwts_nextwrite = 0;
+ fdw_tuplestore->fdwts_lastread = 0;
+ afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = fdw_tuplestore;
+ }
+ return fdw_tuplestore;
+}
+
/* ----------
* afterTriggerCheckState()
*
@@ -3353,7 +3486,8 @@ static void
AfterTriggerExecute(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
- MemoryContext per_tuple_context)
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot)
{
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
@@ -3390,34 +3524,84 @@ AfterTriggerExecute(AfterTriggerEvent event,
/*
* Fetch the required tuple(s).
*/
- if (ItemPointerIsValid(&(event->ate_ctid1)))
+ if (event->ate_flags & AFTER_TRIGGER_FDW)
{
- ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
+ FDWTuplestore *fdw_tuplestore = GetCurrentFDWTuplestore();
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) event;
+
+ /*
+ * Seek to the given index
+ */
+ if (fdw_tuplestore->fdwts_lastread > fdwevent->ate_tupleindex)
+ {
+ if (fdw_tuplestore->fdwts_lastread - fdwevent->ate_tupleindex < fdwevent->ate_tupleindex)
+ {
+ while (fdw_tuplestore->fdwts_lastread > fdwevent->ate_tupleindex)
+ {
+ tuplestore_advance(fdw_tuplestore->fdwts_tuplestore, false);
+ fdw_tuplestore->fdwts_lastread--;
+ }
+ }
+ else
+ {
+ tuplestore_rescan(fdw_tuplestore->fdwts_tuplestore);
+ fdw_tuplestore->fdwts_lastread = 0;
+ }
+ }
+ while (fdw_tuplestore->fdwts_lastread < fdwevent->ate_tupleindex)
+ {
+ tuplestore_advance(fdw_tuplestore->fdwts_tuplestore, true);
+ fdw_tuplestore->fdwts_lastread++;
+ }
+ if (!tuplestore_gettupleslot(fdw_tuplestore->fdwts_tuplestore, true, false, trig_tuple_slot))
+ {
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
- LocTriggerData.tg_trigtuple = &tuple1;
- LocTriggerData.tg_trigtuplebuf = buffer1;
- }
- else
- {
- LocTriggerData.tg_trigtuple = NULL;
+ }
+ fdw_tuplestore->fdwts_lastread++;
+ LocTriggerData.tg_trigtuple = ExecCopySlotTuple(trig_tuple_slot);
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
- }
-
- /* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) &&
- ItemPointerIsValid(&(event->ate_ctid2)))
- {
- ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
- elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
- LocTriggerData.tg_newtuple = &tuple2;
- LocTriggerData.tg_newtuplebuf = buffer2;
+ if (event->ate_flags & AFTER_TRIGGER_2CTIDS)
+ {
+ if (!tuplestore_gettupleslot(fdw_tuplestore->fdwts_tuplestore, true, false, trig_tuple_slot))
+ {
+ elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+ }
+ fdw_tuplestore->fdwts_lastread++;
+ LocTriggerData.tg_newtuple = ExecCopySlotTuple(trig_tuple_slot);
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
}
else
{
- LocTriggerData.tg_newtuple = NULL;
- LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ if (ItemPointerIsValid(&(event->ate_ctid1)))
+ {
+ ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
+ elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+ LocTriggerData.tg_trigtuple = &tuple1;
+ LocTriggerData.tg_trigtuplebuf = buffer1;
+ }
+ else
+ {
+ LocTriggerData.tg_trigtuple = NULL;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ }
+
+ /* don't touch ctid2 if not there */
+ if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) &&
+ ItemPointerIsValid(&(event->ate_ctid2)))
+ {
+ ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
+ elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+ LocTriggerData.tg_newtuple = &tuple2;
+ LocTriggerData.tg_newtuplebuf = buffer2;
+ }
+ else
+ {
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
}
/*
@@ -3559,6 +3743,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
TriggerDesc *trigdesc = NULL;
FmgrInfo *finfo = NULL;
Instrumentation *instr = NULL;
+ TupleTableSlot *slot = NULL;
/* Make a local EState if need be */
if (estate == NULL)
@@ -3574,7 +3759,6 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
-
for_each_chunk(chunk, *events)
{
AfterTriggerEvent event;
@@ -3603,6 +3787,11 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
trigdesc = rInfo->ri_TrigDesc;
finfo = rInfo->ri_TrigFunctions;
instr = rInfo->ri_TrigInstrument;
+ /* Make a slot to read back tuple from the tuplestore */
+ if (slot != NULL)
+ ExecDropSingleTupleTableSlot(slot);
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ slot = MakeSingleTupleTableSlot(rel->rd_att);
if (trigdesc == NULL) /* should not happen */
elog(ERROR, "relation %u has no triggers",
evtshared->ats_relid);
@@ -3614,7 +3803,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
* won't try to re-fire it.
*/
AfterTriggerExecute(event, rel, trigdesc, finfo, instr,
- per_tuple_context);
+ per_tuple_context, slot);
/*
* Mark the event as done.
@@ -3645,6 +3834,9 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
events->tailfree = chunk->freeptr;
}
}
+ if (slot != NULL)
+ ExecDropSingleTupleTableSlot(slot);
+
/* Release working resources */
MemoryContextDelete(per_tuple_context);
@@ -3698,6 +3890,10 @@ AfterTriggerBeginXact(void)
afterTriggers->query_stack = (AfterTriggerEventList *)
MemoryContextAlloc(TopTransactionContext,
8 * sizeof(AfterTriggerEventList));
+ /* Same for the tuplestore used for fdw tuples */
+ afterTriggers->fdw_tuplestores = (FDWTuplestore **)
+ MemoryContextAllocZero(TopTransactionContext,
+ 8 * sizeof(FDWTuplestore *));
afterTriggers->maxquerydepth = 8;
/* Context for events is created only when needed */
@@ -3743,6 +3939,13 @@ AfterTriggerBeginQuery(void)
afterTriggers->query_stack = (AfterTriggerEventList *)
repalloc(afterTriggers->query_stack,
new_alloc * sizeof(AfterTriggerEventList));
+ afterTriggers->fdw_tuplestores = (FDWTuplestore **)
+ repalloc(afterTriggers->fdw_tuplestores,
+ new_alloc * sizeof(FDWTuplestore *));
+ /* Ensure the newly allocated slots are NULL, since they will be */
+ /* initialized lazily. */
+ MemSet(afterTriggers->fdw_tuplestores + (afterTriggers->maxquerydepth * sizeof(FDWTuplestore *)), 0,
+ afterTriggers->maxquerydepth * sizeof(FDWTuplestore *));
afterTriggers->maxquerydepth = new_alloc;
}
@@ -3770,6 +3973,7 @@ void
AfterTriggerEndQuery(EState *estate)
{
AfterTriggerEventList *events;
+ FDWTuplestore *fdw_tuplestore;
/* Must be inside a transaction */
Assert(afterTriggers != NULL);
@@ -3810,9 +4014,18 @@ AfterTriggerEndQuery(EState *estate)
break;
}
+ /*
+ * Release the current query FDW tuples store (if any)
+ */
+ fdw_tuplestore = afterTriggers->fdw_tuplestores[afterTriggers->query_depth];
+ if (fdw_tuplestore)
+ {
+ tuplestore_end(fdw_tuplestore->fdwts_tuplestore);
+ pfree(fdw_tuplestore);
+ afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = NULL;
+ }
/* Release query-local storage for events */
afterTriggerFreeEventList(&afterTriggers->query_stack[afterTriggers->query_depth]);
-
afterTriggers->query_depth--;
}
@@ -3900,8 +4113,9 @@ AfterTriggerEndXact(bool isCommit)
* of memory for the list!
*/
if (afterTriggers && afterTriggers->event_cxt)
+ {
MemoryContextDelete(afterTriggers->event_cxt);
-
+ }
afterTriggers = NULL;
}
@@ -3945,7 +4159,6 @@ AfterTriggerBeginSubXact(void)
afterTriggers->firing_stack = (CommandId *)
palloc(DEFTRIG_INITALLOC * sizeof(CommandId));
afterTriggers->maxtransdepth = DEFTRIG_INITALLOC;
-
MemoryContextSwitchTo(old_cxt);
}
else
@@ -4528,6 +4741,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
+ char relkind = relinfo->ri_RelationDesc->rd_rel->relkind;
int tgtype_event;
int tgtype_level;
int i;
@@ -4550,6 +4764,10 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* arrays.
*/
new_event.ate_flags = 0;
+ if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ {
+ new_event.ate_flags = AFTER_TRIGGER_FDW;
+ }
switch (event)
{
case TRIGGER_EVENT_INSERT:
@@ -4558,8 +4776,25 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup == NULL);
Assert(newtup != NULL);
- ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid1));
- ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) & new_event;
+ FDWTuplestore *fdw_tuplestore = GetCurrentFDWTuplestore();
+
+ if (fdw_tuplestore->fdwts_nextwrite == INT_MAX)
+ {
+ elog(ERROR, "Cannot insert, update or delete more than %i tuples in "
+ "foreign tables with AFTER row-level trigger", INT_MAX);
+ }
+ tuplestore_puttuple(fdw_tuplestore->fdwts_tuplestore, newtup);
+ fdwevent->ate_tupleindex = fdw_tuplestore->fdwts_nextwrite;
+ fdw_tuplestore->fdwts_nextwrite++;
+ }
+ else
+ {
+ ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ }
}
else
{
@@ -4575,8 +4810,25 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup != NULL);
Assert(newtup == NULL);
- ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
- ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) & new_event;
+ FDWTuplestore *fdw_tuplestore = GetCurrentFDWTuplestore();
+
+ if (fdw_tuplestore->fdwts_nextwrite == INT_MAX)
+ {
+ elog(ERROR, "Cannot insert, update or delete more than %i tuples in "
+ "foreign tables with AFTER row-level trigger", INT_MAX);
+ }
+ tuplestore_puttuple(fdw_tuplestore->fdwts_tuplestore, oldtup);
+ fdwevent->ate_tupleindex = fdw_tuplestore->fdwts_nextwrite;
+ fdw_tuplestore->fdwts_nextwrite++;
+ }
+ else
+ {
+ ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ }
}
else
{
@@ -4592,9 +4844,27 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup != NULL);
Assert(newtup != NULL);
- ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
- ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
new_event.ate_flags |= AFTER_TRIGGER_2CTIDS;
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) & new_event;
+ FDWTuplestore *fdw_tuplestore = GetCurrentFDWTuplestore();
+
+ if (fdw_tuplestore->fdwts_nextwrite == INT_MAX)
+ {
+ elog(ERROR, "Cannot insert, update or delete more than %i tuples in "
+ "foreign tables with AFTER row-level trigger", INT_MAX);
+ }
+ tuplestore_puttuple(fdw_tuplestore->fdwts_tuplestore, oldtup);
+ tuplestore_puttuple(fdw_tuplestore->fdwts_tuplestore, newtup);
+ fdwevent->ate_tupleindex = fdw_tuplestore->fdwts_nextwrite;
+ fdw_tuplestore->fdwts_nextwrite += 2;
+ }
+ else
+ {
+ ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
+ }
}
else
{
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 6f0f47e..80c6249 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -488,7 +488,7 @@ ldelete:;
(estate->es_processed)++;
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, planSlot);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
@@ -788,7 +788,7 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
+ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, planSlot,
recheckIndexes);
list_free(recheckIndexes);
@@ -986,7 +986,20 @@ ExecModifyTable(ModifyTableState *node)
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
- /* do nothing; FDW must fetch any junk attrs it wants */
+ /*
+ * If the junkAttNo is valid, then it identifies the
+ * wholerow attribute. This is the case when there is an
+ * UPDATE or DELETE trigger.
+ */
+ if (AttributeNumberIsValid(junkfilter->jf_junkAttNo))
+ {
+ datum = ExecGetJunkAttribute(slot,
+ junkfilter->jf_junkAttNo,
+ &isNull);
+ if (isNull)
+ elog(ERROR, "wholerow is NULL");
+ oldtuple = DatumGetHeapTupleHeader(datum);
+ }
}
else
{
@@ -1334,7 +1347,14 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
- /* FDW must fetch any junk attrs it wants */
+ /*
+ * When there is an AFTER trigger, there should be a
+ * wholerow attribute.
+ */
+ AttrNumber junkAttNo = ExecFindJunkAttribute(j, "wholerow");
+
+ if (AttributeNumberIsValid(junkAttNo))
+ j->jf_junkAttNo = junkAttNo;
}
else
{
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 35bda67..52cc5b7 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -240,7 +240,22 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->commandType = parse->commandType;
result->queryId = parse->queryId;
- result->hasReturning = (parse->returningList != NIL);
+
+ /*
+ * Mark the result as having RETURNING only if the returning target list
+ * has non-resjunk entries
+ */
+ result->hasReturning = false;
+ foreach(lp, parse->returningList)
+ {
+ TargetEntry *tle = lfirst(lp);
+
+ if (!tle->resjunk)
+ {
+ result->hasReturning = true;
+ break;
+ }
+ }
result->hasModifyingCTE = parse->hasModifyingCTE;
result->canSetTag = parse->canSetTag;
result->transientPlan = glob->transientPlan;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 0b13645..6489548 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -45,7 +45,8 @@ static Query *rewriteRuleAction(Query *parsetree,
CmdType event,
bool *returning_flag);
static List *adjustJoinTreeList(Query *parsetree, bool removert, int rt_index);
-static void rewriteTargetListIU(Query *parsetree, Relation target_relation,
+static void rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte,
+ Relation target_relation,
List **attrno_list);
static TargetEntry *process_matched_tle(TargetEntry *src_tle,
TargetEntry *prior_tle,
@@ -642,6 +643,10 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
* 4. Sort the tlist into standard order: non-junk fields in order by resno,
* then junk fields (these in no particular order).
*
+ * 5. For an insert on a foreign table with an after trigger, add missing
+ * attribute to the returning targetlist. This is needed to ensure that all
+ * attributes are fetched from the remote side on a returning statement.
+ *
* We must do items 1,2,3 before firing rewrite rules, else rewritten
* references to NEW.foo will produce wrong or incomplete results. Item 4
* is not needed for rewriting, but will be needed by the planner, and we
@@ -653,8 +658,8 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
* processing VALUES RTEs.
*/
static void
-rewriteTargetListIU(Query *parsetree, Relation target_relation,
- List **attrno_list)
+rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte,
+ Relation target_relation, List **attrno_list)
{
CmdType commandType = parsetree->commandType;
TargetEntry **new_tles;
@@ -730,6 +735,28 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
}
}
+ /*
+ * For foreign tables, force RETURNING the whole-row if a corresponding
+ * AFTER trigger is found
+ */
+ if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE &&
+ target_relation->trigdesc &&
+ ((commandType == CMD_INSERT && target_relation->trigdesc->trig_insert_after_row) ||
+ (commandType == CMD_UPDATE && target_relation->trigdesc->trig_update_after_row)))
+
+ {
+ Var *var = makeWholeRowVar(target_rte,
+ parsetree->resultRelation,
+ 0,
+ false);
+ TargetEntry *tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->returningList) + 1,
+ "wholerow",
+ true);
+
+ parsetree->returningList = lappend(parsetree->returningList, tle);
+ }
+
for (attrno = 1; attrno <= numattrs; attrno++)
{
TargetEntry *new_tle = new_tles[attrno - 1];
@@ -818,7 +845,6 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
}
pfree(new_tles);
-
parsetree->targetList = list_concat(new_tlist, junk_tlist);
}
@@ -1174,7 +1200,7 @@ static void
rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
Relation target_relation)
{
- Var *var;
+ Var *var = NULL;
const char *attrname;
TargetEntry *tle;
@@ -1206,7 +1232,27 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
target_relation);
- return;
+ /*
+ * If we have a trigger corresponding to the operation, add a wholerow
+ * attribute.
+ */
+ if (target_relation->trigdesc &&
+ ((parsetree->commandType == CMD_UPDATE &&
+ (target_relation->trigdesc->trig_update_after_row
+ || target_relation->trigdesc->trig_update_before_row)) ||
+ (parsetree->commandType == CMD_DELETE &&
+ (target_relation->trigdesc->trig_delete_after_row ||
+ target_relation->trigdesc->trig_delete_before_row))))
+ {
+ var = makeWholeRowVar(target_rte,
+ parsetree->resultRelation,
+ 0,
+ false);
+
+ attrname = "wholerow";
+
+ }
+
}
else
{
@@ -1221,13 +1267,15 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
attrname = "wholerow";
}
+ if (var != NULL)
+ {
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->targetList) + 1,
+ pstrdup(attrname),
+ true);
- tle = makeTargetEntry((Expr *) var,
- list_length(parsetree->targetList) + 1,
- pstrdup(attrname),
- true);
-
- parsetree->targetList = lappend(parsetree->targetList, tle);
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+ }
}
@@ -2965,19 +3013,19 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
List *attrnos;
/* Process the main targetlist ... */
- rewriteTargetListIU(parsetree, rt_entry_relation, &attrnos);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, &attrnos);
/* ... and the VALUES expression lists */
rewriteValuesRTE(values_rte, rt_entry_relation, attrnos);
}
else
{
/* Process just the main targetlist */
- rewriteTargetListIU(parsetree, rt_entry_relation, NULL);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL);
}
}
else if (event == CMD_UPDATE)
{
- rewriteTargetListIU(parsetree, rt_entry_relation, NULL);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL);
rewriteTargetListUD(parsetree, rt_entry, rt_entry_relation);
}
else if (event == CMD_DELETE)
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 44d686c..ed598f0 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -150,7 +150,8 @@ extern bool ExecBRDeleteTriggers(EState *estate,
ItemPointer tupleid);
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ TupleTableSlot *slot);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -167,6 +168,7 @@ extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple newtuple,
+ TupleTableSlot *slot,
List *recheckIndexes);
extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 60506e0..3405b6c 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -1158,6 +1158,36 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
ERROR: must be owner of foreign server s9
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+ERROR: "foreign_table_1" is a foreign table
+DETAIL: Foreign Tables cannot have constraint triggers.
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+DROP FUNCTION dummy_trigger();
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
ERROR: foreign table "no_table" does not exist
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index f819eb1..6166097 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -470,6 +470,43 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+
+DROP FUNCTION dummy_trigger();
+
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
DROP FOREIGN TABLE IF EXISTS no_table;
On Sun, Feb 02, 2014 at 11:53:51AM +0100, Ronan Dunklau wrote:
Le jeudi 30 janvier 2014 14:05:08 Noah Misch a �crit :
On Thu, Jan 23, 2014 at 03:17:35PM +0100, Ronan Dunklau wrote:
What do you think about this approach ? Is there something I missed which
would make it not sustainable ?Seems basically reasonable. I foresee multiple advantages from having one
tuplestore per query level as opposed to one for the entire transaction.
You would remove the performance trap of backing up the tuplestore by
rescanning. It permits reclaiming memory and disk space in
AfterTriggerEndQuery() rather than at end of transaction. You could remove
ate_ptr1 and ate_ptr2 from AfterTriggerEventDataFDW and just store the
flags word: depending on AFTER_TRIGGER_2CTIDS, grab either the next one or
the next two tuples from the tuplestore. Using work_mem per
AfterTriggerBeginQuery() instead of per transaction is no problem. What do
you think of that design change?I agree that this design is better, but I have some objections.
We can remove ate_ptr2 and rely on the AFTER_TRIGGER_2CTIDS flag, but the
rescanning and ate_ptr1 (renamed ate_tupleindex in the attached patch) can't
go away.Consider for example the case of a foreign table with more than one AFTER
UPDATE triggers. Unless we store the tuples once for each trigger, we will
have to rescan the tuplestore.
Will we? Within a given query level, when do (non-deferred) triggers execute
in an order other than the enqueue order?
To mitigate the effects of this behaviour, I added the option to perform a
reverse_seek when the looked-up tuple is nearer from the current index than
from the start.
If there's still a need to seek within the tuplestore, that should get rid of
the O(n^2) effect. I'm hoping that per-query-level tuplestores will eliminate
the need to seek entirely.
If you do pursue that change, make sure the code still does the right thing
when it drops queued entries during subxact abort.I don't really understand what should be done at that stage. Since triggers on
foreign tables are not allowed to be deferred, everything should be cleaned up
at the end of each query, right ? So, there shouldn't be any queued entries.
I suspect that's right. If you haven't looked over AfterTriggerEndSubXact(),
please do so and ensure all its actions still make sense in the context of
this new kind of trigger storage.
The attached patch checks this, and add documentation for this limitation.
I'm not really sure about how to phrase that correctly in the error
message
and the documentation. One can store at most INT_MAX foreign tuples, which
means that at most INT_MAX insert or delete or "half-updates" can occur.
By
half-updates, I mean that for update two tuples are stored whereas in
contrast to only one for insert and delete trigger.Besides, I don't know where this disclaimer should be in the
documentation.
Any advice here ?I wouldn't mention that limitation.
Maybe it shouldn't be so prominent, but I still think a note somewhere
couldn't hurt.
Perhaps. There's not much documentation of such implementation upper limits,
and there's no usage of "INT_MAX" outside of parts that discuss writing C
code. I'm not much of a visionary when it comes to the documentation; I try
to document new things with an amount of detail similar to old features.
Should the use of work_mem be documented somewhere, too ?
I wouldn't. Most uses of work_mem are undocumented, even relatively major
ones like count(DISTINCT ...) and CTEs. So, while I'd generally favor a patch
documenting all/most of the things that use work_mem, it would be odd to
document one new consumer apart from the others.
This is the performance trap I mentioned above; it brings potential O(n^2)
complexity to certain AFTER trigger execution scenarios.What scenarios do you have in mind ? I "fixed" the problem when there are
multiple triggers on a foreign table, do you have any other one ?
I'm not aware of such a performance trap in your latest design. For what it's
worth, I don't think multiple triggers were ever a problem. In the previous
design, a problem arose if you had a scenario like this:
Query level 1: queue one million events
...
Repeat this section many times:
Query level 2: queue one event
Query level 3: queue one event
Query level 3: execute events
Query level 2: execute events <-- had to advance through the 1M stored events
...
Query level 1: execute events
Thanks,
nm
--
Noah Misch
EnterpriseDB http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Le lundi 3 février 2014 23:28:45 Noah Misch a écrit :
On Sun, Feb 02, 2014 at 11:53:51AM +0100, Ronan Dunklau wrote:
Le jeudi 30 janvier 2014 14:05:08 Noah Misch a écrit :
On Thu, Jan 23, 2014 at 03:17:35PM +0100, Ronan Dunklau wrote:
What do you think about this approach ? Is there something I missed
which
would make it not sustainable ?Seems basically reasonable. I foresee multiple advantages from having
one
tuplestore per query level as opposed to one for the entire transaction.
You would remove the performance trap of backing up the tuplestore by
rescanning. It permits reclaiming memory and disk space in
AfterTriggerEndQuery() rather than at end of transaction. You could
remove
ate_ptr1 and ate_ptr2 from AfterTriggerEventDataFDW and just store the
flags word: depending on AFTER_TRIGGER_2CTIDS, grab either the next one
or
the next two tuples from the tuplestore. Using work_mem per
AfterTriggerBeginQuery() instead of per transaction is no problem. What
do
you think of that design change?I agree that this design is better, but I have some objections.
We can remove ate_ptr2 and rely on the AFTER_TRIGGER_2CTIDS flag, but the
rescanning and ate_ptr1 (renamed ate_tupleindex in the attached patch)
can't go away.Consider for example the case of a foreign table with more than one AFTER
UPDATE triggers. Unless we store the tuples once for each trigger, we will
have to rescan the tuplestore.Will we? Within a given query level, when do (non-deferred) triggers
execute in an order other than the enqueue order?
Let me explain what I had in mind.
Looking at the code in AfterTriggerSaveEvent:
- we build a "template" AfterTriggerEvent, and store the tuple(s)
- for each suitable after trigger that matches the trigger type, as well as
the WHEN condition if any, a copy of the previously built AfterTriggerEvent is
queued
Later, those events are fired in order.
This means that more than one event can be fired for one tuple.
Take this example:
CREATE TRIGGER trig_row_after1
AFTER UPDATE ON rem2
FOR EACH ROW
WHEN (NEW.f1 % 5 < 3)
EXECUTE PROCEDURE trigger_func('TRIG1');
CREATE TRIGGER trig_row_after2
AFTER UPDATE ON rem2
FOR EACH ROW
WHEN (NEW.f1 % 5 < 4)
EXECUTE PROCEDURE trigger_func('TRIG2');
UPDATE rem2 set f2 = 'something';
Assuming 5 rows with f1 as a serial, the fired AfterTriggerEvent's
ate_tupleindex will be, in that order. Ass
0-0-2-2-4-8-8
So, at least a backward seek is required for trig_row_after2 to be able to
retrieve a tuple that was already consumed when firing trig_row_after1.
On a side note, this made me realize that it is better to avoid storing a
tuple entirely if there is no enabled trigger (the f1 = 4 case above). The
attached patch does that, so the previous sequence becomes:
0-0-2-2-4-6-6
It also prevents from initalizing a tuplestore at all if its not needed.
To mitigate the effects of this behaviour, I added the option to perform a
reverse_seek when the looked-up tuple is nearer from the current index
than
from the start.If there's still a need to seek within the tuplestore, that should get rid
of the O(n^2) effect. I'm hoping that per-query-level tuplestores will
eliminate the need to seek entirely.
I think the only case when seeking is still needed is when there are more than
one after trigger that need to be fired, since the abovementioned change
prevents from seeking to skip tuples.
If you do pursue that change, make sure the code still does the right
thing
when it drops queued entries during subxact abort.I don't really understand what should be done at that stage. Since
triggers on foreign tables are not allowed to be deferred, everything
should be cleaned up at the end of each query, right ? So, there
shouldn't be any queued entries.I suspect that's right. If you haven't looked over
AfterTriggerEndSubXact(), please do so and ensure all its actions still
make sense in the context of this new kind of trigger storage.
You're right, I missed something here. When aborting a subxact, the
tuplestores for queries below the subxact query depth should be cleaned, if
any, because AfterTriggerEndQuery has not been called for the failing query.
The attached patch fixes that.
The attached patch checks this, and add documentation for this
limitation.
I'm not really sure about how to phrase that correctly in the error
message
and the documentation. One can store at most INT_MAX foreign tuples,
which
means that at most INT_MAX insert or delete or "half-updates" can
occur.
By
half-updates, I mean that for update two tuples are stored whereas in
contrast to only one for insert and delete trigger.Besides, I don't know where this disclaimer should be in the
documentation.
Any advice here ?I wouldn't mention that limitation.
Maybe it shouldn't be so prominent, but I still think a note somewhere
couldn't hurt.Perhaps. There's not much documentation of such implementation upper
limits, and there's no usage of "INT_MAX" outside of parts that discuss
writing C code. I'm not much of a visionary when it comes to the
documentation; I try to document new things with an amount of detail
similar to old features.
Ok, I removed the paragraph documenting the limitation.
Should the use of work_mem be documented somewhere, too ?
I wouldn't. Most uses of work_mem are undocumented, even relatively major
ones like count(DISTINCT ...) and CTEs. So, while I'd generally favor a
patch documenting all/most of the things that use work_mem, it would be odd
to document one new consumer apart from the others.
Ok.
This is the performance trap I mentioned above; it brings potential
O(n^2)
complexity to certain AFTER trigger execution scenarios.What scenarios do you have in mind ? I "fixed" the problem when there are
multiple triggers on a foreign table, do you have any other one ?I'm not aware of such a performance trap in your latest design.
Good !
Thanks,
nm
--
Ronan Dunklau
http://dalibo.com - http://dalibo.org
Attachments:
foreign_trigger_v8.patchtext/x-patch; charset=UTF-8; name=foreign_trigger_v8.patchDownload
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 38c6cf8..f77144f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2372,3 +2372,318 @@ select * from rem1;
11 | bye remote
(4 rows)
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %', tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+delete from rem1;
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = AFTER, level = STATEMENT
+insert into rem1 values(1,'insert');
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = AFTER, level = STATEMENT
+update rem1 set f2 = 'update' where f1 = 1;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+update rem1 set f2 = f2 || f2;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+DELETE from rem1;
+-- Test WHEN conditions
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+NOTICE: trig_row_before_delete(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+NOTICE: trig_row_after_delete(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+-- Test various RETURN statements in BEFORE triggers.
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------
+ insert triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+ 2 | insert triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------
+ 1 | triggered !
+ 2 | triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------
+ skidoo triggered !
+ skidoo triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | skidoo triggered !
+ 2 | skidoo triggered !
+(2 rows)
+
+DELETE FROM rem1;
+-- Add a second trigger, to check that the changes are propagated corretly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------------------
+ insert triggered ! triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+ 2 | insert triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------
+ 1 | triggered ! triggered !
+ 2 | triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------------------
+ skidoo triggered ! triggered !
+ skidoo triggered ! triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | skidoo triggered ! triggered !
+ 2 | skidoo triggered ! triggered !
+(2 rows)
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+DELETE from rem1;
+INSERT INTO rem1 VALUES (1, 'test');
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+UPDATE rem1 SET f2 = 'test2';
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DELETE from rem1;
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1(f2) VALUES ('test');
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (12,test)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (12,"test triggered !")
+UPDATE rem1 SET f2 = 'testo';
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,testo)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,"testo triggered !")
+-- Test returning system attributes
+INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax;
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (13,test)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (13,"test triggered !")
+ ctid | xmin | xmax
+--------+------+------------
+ (0,27) | 180 | 4294967295
+(1 row)
+
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index ce8bb75..54b35d9 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -384,3 +384,215 @@ insert into loc1(f2) values('bye');
insert into rem1(f2) values('bye remote');
select * from loc1;
select * from rem1;
+
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %', tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+delete from rem1;
+insert into rem1 values(1,'insert');
+update rem1 set f2 = 'update' where f1 = 1;
+update rem1 set f2 = f2 || f2;
+
+
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+
+DELETE from rem1;
+
+
+-- Test WHEN conditions
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+
+
+-- Test various RETURN statements in BEFORE triggers.
+
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DELETE FROM rem1;
+
+-- Add a second trigger, to check that the changes are propagated corretly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+
+DELETE from rem1;
+
+INSERT INTO rem1 VALUES (1, 'test');
+
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+
+SELECT * from loc1;
+
+UPDATE rem1 SET f2 = 'test2';
+
+SELECT * from loc1;
+
+DELETE from rem1;
+
+SELECT * from loc1;
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1(f2) VALUES ('test');
+UPDATE rem1 SET f2 = 'testo';
+
+-- Test returning system attributes
+INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax;
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index e5ec738..07bc757 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -93,7 +93,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<para>
The following table summarizes which types of triggers may be used on
- tables and views:
+ tables, views and foreign tables:
</para>
<informaltable id="supported-trigger-types">
@@ -110,8 +110,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>BEFORE</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -121,8 +121,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>AFTER</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables, foreign tables</entry>
+ <entry align="center">Tables, views and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -244,7 +244,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
<term><replaceable class="parameter">table_name</replaceable></term>
<listitem>
<para>
- The name (optionally schema-qualified) of the table or view the trigger
+ The name (optionally schema-qualified) of the table, view or foreign table the trigger
is for.
</para>
</listitem>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index f579340..f115bdc 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -33,11 +33,11 @@
<para>
A trigger is a specification that the database should automatically
execute a particular function whenever a certain type of operation is
- performed. Triggers can be attached to both tables and views.
+ performed. Triggers can be attached to tables, views and foreign tables.
</para>
<para>
- On tables, triggers can be defined to execute either before or after any
+ On tables and foreign tables, triggers can be defined to execute either before or after any
<command>INSERT</command>, <command>UPDATE</command>, or
<command>DELETE</command> operation, either once per modified row,
or once per <acronym>SQL</acronym> statement.
@@ -46,7 +46,7 @@
<command>UPDATE</command> statement.
Triggers can also fire for <command>TRUNCATE</command> statements.
If a trigger event occurs, the trigger's function is called at the
- appropriate time to handle the event.
+ appropriate time to handle the event. Foreign tables do not support the TRUNCATE statement at all.
</para>
<para>
@@ -108,11 +108,11 @@
statement starts to do anything, while statement-level <literal>AFTER</>
triggers fire at the very end of the statement. These types of
triggers may be defined on tables or views. Row-level <literal>BEFORE</>
- triggers fire immediately before a particular row is operated on,
- while row-level <literal>AFTER</> triggers fire at the end of the
- statement (but before any statement-level <literal>AFTER</> triggers).
- These types of triggers may only be defined on tables. Row-level
- <literal>INSTEAD OF</> triggers may only be defined on views, and fire
+ triggers fire immediately before a particular row is operated on.
+ For regular and foreign tables, row-level <literal>AFTER</> triggers fire at
+ the end of the statement (but before any statement-level <literal>AFTER</> triggers).
+ These types of triggers may only be defined on tables and foreign tables.
+ Row-level <literal>INSTEAD OF</> triggers may only be defined on views, and fire
immediately as each row in the view is identified as needing to be
operated on.
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 08b037e..32b976a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3175,6 +3175,9 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DisableTrig: /* DISABLE TRIGGER variants */
case AT_DisableTrigAll:
case AT_DisableTrigUser:
+ ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE);
+ pass = AT_PASS_MISC;
+ break;
case AT_EnableRule: /* ENABLE/DISABLE RULE variants */
case AT_EnableAlwaysRule:
case AT_EnableReplicaRule:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 86449a6..c20de0c 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -55,6 +55,7 @@
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/tqual.h"
+#include "utils/tuplestore.h"
/* GUC variables */
@@ -75,6 +76,9 @@ static HeapTuple GetTupleForTrigger(EState *estate,
ItemPointer tid,
LockTupleMode lockmode,
TupleTableSlot **newSlot);
+
+static HeapTuple ExtractOldTuple(TupleTableSlot *mixedtupleslot,
+ ResultRelInfo *relinfo);
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
Trigger *trigger, TriggerEvent event,
Bitmapset *modifiedCols,
@@ -184,12 +188,22 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
RelationGetRelationName(rel)),
errdetail("Views cannot have TRUNCATE triggers.")));
}
+ else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ 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("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
-
+ }
if (!allowSystemTableMods && IsSystemRelation(rel))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1062,10 +1076,11 @@ RemoveTriggerById(Oid trigOid)
rel = heap_open(relid, AccessExclusiveLock);
if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_VIEW)
+ rel->rd_rel->relkind != RELKIND_VIEW &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view",
+ errmsg("\"%s\" is not a table, view or foreign table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
@@ -1166,10 +1181,11 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
form = (Form_pg_class) GETSTRUCT(tuple);
/* only tables and views can have triggers */
- if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW)
+ if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW &&
+ form->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view", rv->relname)));
+ errmsg("\"%s\" is not a table, view or foreign table", rv->relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
@@ -1844,9 +1860,7 @@ ExecCallTriggerFunc(TriggerData *trigdata,
*/
InitFunctionCallInfoData(fcinfo, finfo, 0,
InvalidOid, (Node *) trigdata, NULL);
-
pgstat_init_function_usage(&fcinfo, &fcusage);
-
MyTriggerDepth++;
PG_TRY();
{
@@ -2155,9 +2169,18 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
HeapTuple newtuple;
TupleTableSlot *newSlot;
int i;
+ Relation rel = relinfo->ri_RelationDesc;
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- LockTupleExclusive, &newSlot);
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ LockTupleExclusive, &newSlot);
+ }
if (trigtuple == NULL)
return false;
@@ -2204,19 +2227,33 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
void
ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ TupleTableSlot *slot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since
+ * there cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ trigtuple = ExtractOldTuple(slot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ }
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
true, trigtuple, NULL, NIL, NULL);
heap_freetuple(trigtuple);
@@ -2335,7 +2372,8 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TupleTableSlot *
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid, TupleTableSlot *slot)
+ ItemPointer tupleid,
+ TupleTableSlot *slot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
HeapTuple slottuple = ExecMaterializeSlot(slot);
@@ -2345,6 +2383,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
HeapTuple oldtuple;
TupleTableSlot *newSlot;
int i;
+ Relation relation = relinfo->ri_RelationDesc;
Bitmapset *modifiedCols;
Bitmapset *keyCols;
LockTupleMode lockmode;
@@ -2355,7 +2394,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
* concurrency.
*/
modifiedCols = GetModifiedColumns(relinfo, estate);
- keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
+ keyCols = RelationGetIndexAttrBitmap(relation,
INDEX_ATTR_BITMAP_KEY);
if (bms_overlap(keyCols, modifiedCols))
lockmode = LockTupleExclusive;
@@ -2363,8 +2402,16 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
lockmode = LockTupleNoKeyExclusive;
/* get a copy of the on-disk tuple we are planning to update */
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- lockmode, &newSlot);
+ if (relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ newSlot = NULL;
+ trigtuple = ExtractOldTuple(epqstate->origslot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ lockmode, &newSlot);
+ }
if (trigtuple == NULL)
return NULL; /* cancel the update action */
@@ -2446,24 +2493,40 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid, HeapTuple newtuple,
+ ItemPointer tupleid,
+ HeapTuple newtuple,
+ TupleTableSlot *slot,
List *recheckIndexes)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ Relation rel = relinfo->ri_RelationDesc;
+ HeapTuple trigtuple;
+ /*
+ * For FOREIGN Table, after triggers are fired immediately, since
+ * there cannot be any constraint triggers.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ trigtuple = ExtractOldTuple(slot, relinfo);
+ }
+ else
+ {
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ }
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
true, trigtuple, newtuple, recheckIndexes,
GetModifiedColumns(relinfo, estate));
heap_freetuple(trigtuple);
+
}
}
@@ -2731,6 +2794,33 @@ ltrmark:;
}
/*
+ * Get an old tuple from a "mixed tuple", containing both the new values as well
+ * as well as the old ones as resjunk columns.
+ */
+static HeapTuple
+ExtractOldTuple(TupleTableSlot *planSlot,
+ ResultRelInfo *relinfo)
+{
+ bool isNull;
+ JunkFilter *junkfilter = relinfo->ri_junkFilter;
+ HeapTuple oldtuple = palloc0(sizeof(HeapTupleData));
+ HeapTupleHeader td;
+ Datum datum = ExecGetJunkAttribute(planSlot,
+ junkfilter->jf_junkAttNo,
+ &isNull);
+
+ /* shouldn't ever get a null result... */
+ if (isNull)
+ elog(ERROR, "wholerow is NULL");
+ td = DatumGetHeapTupleHeader(datum);
+ oldtuple->t_len = HeapTupleHeaderGetDatumLength(td);
+ oldtuple->t_data = td;
+ oldtuple->t_tableOid = RelationGetRelid(relinfo->ri_RelationDesc);
+ return oldtuple;
+}
+
+
+/*
* Is trigger enabled to fire?
*/
static bool
@@ -2948,6 +3038,7 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_2CTIDS 0x10000000
#define AFTER_TRIGGER_DONE 0x20000000
#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
+#define AFTER_TRIGGER_FDW 0x80000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -2975,9 +3066,17 @@ typedef struct AfterTriggerEventDataOneCtid
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
} AfterTriggerEventDataOneCtid;
+typedef struct AfterTriggerEventDataFDW
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ int ate_tupleindex; /* index of the first tuple */
+} AfterTriggerEventDataFDW;
+
#define SizeofTriggerEvent(evt) \
(((evt)->ate_flags & AFTER_TRIGGER_2CTIDS) ? \
- sizeof(AfterTriggerEventData) : sizeof(AfterTriggerEventDataOneCtid))
+ sizeof(AfterTriggerEventData) : \
+ ((evt)->ate_flags & AFTER_TRIGGER_FDW) ? sizeof(AfterTriggerEventDataFDW) : \
+ sizeof(AfterTriggerEventDataOneCtid))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3020,6 +3119,14 @@ typedef struct AfterTriggerEventList
for_each_chunk(cptr, evtlist) for_each_event(eptr, cptr)
+/* A wrapper around Tuplestorestate to keep the last read/written positions */
+typedef struct FDWTuplestore
+{
+ Tuplestorestate *fdwts_tuplestore;
+ int fdwts_nextwrite;
+ int fdwts_lastread;
+} FDWTuplestore;
+
/*
* All per-transaction data for the AFTER TRIGGERS module.
*
@@ -3050,7 +3157,11 @@ typedef struct AfterTriggerEventList
* immediate-mode triggers, and append any deferred events to the main events
* list.
*
- * maxquerydepth is just the allocated length of query_stack.
+ * fdw_tuplestores[query_depth] is a list of FDWTuplestore containing the
+ * foreign tuples needed for the current query.
+ *
+ * maxquerydepth is just the allocated length of query_stack and
+ * fdw_tuplestores.
*
* state_stack is a stack of pointers to saved copies of the SET CONSTRAINTS
* state data; each subtransaction level that modifies that state first
@@ -3071,6 +3182,7 @@ typedef struct AfterTriggerEventList
* each stack. (By not keeping our own stack pointer, we can avoid trouble
* in cases where errors during subxact abort cause multiple invocations
* of AfterTriggerEndSubXact() at the same nesting depth.)
+ *
*/
typedef struct AfterTriggersData
{
@@ -3089,24 +3201,45 @@ typedef struct AfterTriggersData
int *depth_stack; /* stacked query_depths */
CommandId *firing_stack; /* stacked firing_counters */
int maxtransdepth; /* allocated len of above arrays */
+ FDWTuplestore **fdw_tuplestores; /* tuplestore for each query */
} AfterTriggersData;
typedef AfterTriggersData *AfterTriggers;
static AfterTriggers afterTriggers;
-
static void AfterTriggerExecute(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
- MemoryContext per_tuple_context);
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot);
static SetConstraintState SetConstraintStateCreate(int numalloc);
static SetConstraintState SetConstraintStateCopy(SetConstraintState state);
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
Oid tgoid, bool tgisdeferred);
+/*
+ * Gets the current query fdwtuple store, and inializes it
+ * if necessary
+ */
+static FDWTuplestore *
+GetCurrentFDWTuplestore()
+{
+ FDWTuplestore *fdw_tuplestore = afterTriggers->fdw_tuplestores[afterTriggers->query_depth];
+
+ if (fdw_tuplestore == NULL)
+ {
+ fdw_tuplestore = palloc(sizeof(FDWTuplestore));
+ fdw_tuplestore->fdwts_tuplestore = tuplestore_begin_heap(true, false, work_mem);
+ fdw_tuplestore->fdwts_nextwrite = 0;
+ fdw_tuplestore->fdwts_lastread = 0;
+ afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = fdw_tuplestore;
+ }
+ return fdw_tuplestore;
+}
+
/* ----------
* afterTriggerCheckState()
*
@@ -3353,7 +3486,8 @@ static void
AfterTriggerExecute(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
- MemoryContext per_tuple_context)
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot)
{
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
@@ -3390,34 +3524,84 @@ AfterTriggerExecute(AfterTriggerEvent event,
/*
* Fetch the required tuple(s).
*/
- if (ItemPointerIsValid(&(event->ate_ctid1)))
+ if (event->ate_flags & AFTER_TRIGGER_FDW)
{
- ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
+ FDWTuplestore *fdw_tuplestore = GetCurrentFDWTuplestore();
+ AfterTriggerEventDataFDW *fdwevent = (AfterTriggerEventDataFDW *) event;
+
+ /*
+ * Seek to the given index
+ */
+ if (fdw_tuplestore->fdwts_lastread > fdwevent->ate_tupleindex)
+ {
+ if (fdw_tuplestore->fdwts_lastread - fdwevent->ate_tupleindex < fdwevent->ate_tupleindex)
+ {
+ while (fdw_tuplestore->fdwts_lastread > fdwevent->ate_tupleindex)
+ {
+ tuplestore_advance(fdw_tuplestore->fdwts_tuplestore, false);
+ fdw_tuplestore->fdwts_lastread--;
+ }
+ }
+ else
+ {
+ tuplestore_rescan(fdw_tuplestore->fdwts_tuplestore);
+ fdw_tuplestore->fdwts_lastread = 0;
+ }
+ }
+ while (fdw_tuplestore->fdwts_lastread < fdwevent->ate_tupleindex)
+ {
+ tuplestore_advance(fdw_tuplestore->fdwts_tuplestore, true);
+ fdw_tuplestore->fdwts_lastread++;
+ }
+ if (!tuplestore_gettupleslot(fdw_tuplestore->fdwts_tuplestore, true, false, trig_tuple_slot))
+ {
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
- LocTriggerData.tg_trigtuple = &tuple1;
- LocTriggerData.tg_trigtuplebuf = buffer1;
- }
- else
- {
- LocTriggerData.tg_trigtuple = NULL;
+ }
+ fdw_tuplestore->fdwts_lastread++;
+ LocTriggerData.tg_trigtuple = ExecCopySlotTuple(trig_tuple_slot);
LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
- }
-
- /* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) &&
- ItemPointerIsValid(&(event->ate_ctid2)))
- {
- ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
- elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
- LocTriggerData.tg_newtuple = &tuple2;
- LocTriggerData.tg_newtuplebuf = buffer2;
+ if (event->ate_flags & AFTER_TRIGGER_2CTIDS)
+ {
+ if (!tuplestore_gettupleslot(fdw_tuplestore->fdwts_tuplestore, true, false, trig_tuple_slot))
+ {
+ elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+ }
+ fdw_tuplestore->fdwts_lastread++;
+ LocTriggerData.tg_newtuple = ExecCopySlotTuple(trig_tuple_slot);
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
}
else
{
- LocTriggerData.tg_newtuple = NULL;
- LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ if (ItemPointerIsValid(&(event->ate_ctid1)))
+ {
+ ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
+ elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+ LocTriggerData.tg_trigtuple = &tuple1;
+ LocTriggerData.tg_trigtuplebuf = buffer1;
+ }
+ else
+ {
+ LocTriggerData.tg_trigtuple = NULL;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ }
+
+ /* don't touch ctid2 if not there */
+ if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) &&
+ ItemPointerIsValid(&(event->ate_ctid2)))
+ {
+ ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
+ elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+ LocTriggerData.tg_newtuple = &tuple2;
+ LocTriggerData.tg_newtuplebuf = buffer2;
+ }
+ else
+ {
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
}
/*
@@ -3559,6 +3743,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
TriggerDesc *trigdesc = NULL;
FmgrInfo *finfo = NULL;
Instrumentation *instr = NULL;
+ TupleTableSlot *slot = NULL;
/* Make a local EState if need be */
if (estate == NULL)
@@ -3574,7 +3759,6 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
-
for_each_chunk(chunk, *events)
{
AfterTriggerEvent event;
@@ -3603,6 +3787,11 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
trigdesc = rInfo->ri_TrigDesc;
finfo = rInfo->ri_TrigFunctions;
instr = rInfo->ri_TrigInstrument;
+ /* Make a slot to read back tuple from the tuplestore */
+ if (slot != NULL)
+ ExecDropSingleTupleTableSlot(slot);
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ slot = MakeSingleTupleTableSlot(rel->rd_att);
if (trigdesc == NULL) /* should not happen */
elog(ERROR, "relation %u has no triggers",
evtshared->ats_relid);
@@ -3614,7 +3803,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
* won't try to re-fire it.
*/
AfterTriggerExecute(event, rel, trigdesc, finfo, instr,
- per_tuple_context);
+ per_tuple_context, slot);
/*
* Mark the event as done.
@@ -3645,6 +3834,9 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
events->tailfree = chunk->freeptr;
}
}
+ if (slot != NULL)
+ ExecDropSingleTupleTableSlot(slot);
+
/* Release working resources */
MemoryContextDelete(per_tuple_context);
@@ -3698,6 +3890,10 @@ AfterTriggerBeginXact(void)
afterTriggers->query_stack = (AfterTriggerEventList *)
MemoryContextAlloc(TopTransactionContext,
8 * sizeof(AfterTriggerEventList));
+ /* Same for the tuplestore used for fdw tuples */
+ afterTriggers->fdw_tuplestores = (FDWTuplestore **)
+ MemoryContextAllocZero(TopTransactionContext,
+ 8 * sizeof(FDWTuplestore *));
afterTriggers->maxquerydepth = 8;
/* Context for events is created only when needed */
@@ -3743,6 +3939,13 @@ AfterTriggerBeginQuery(void)
afterTriggers->query_stack = (AfterTriggerEventList *)
repalloc(afterTriggers->query_stack,
new_alloc * sizeof(AfterTriggerEventList));
+ afterTriggers->fdw_tuplestores = (FDWTuplestore **)
+ repalloc(afterTriggers->fdw_tuplestores,
+ new_alloc * sizeof(FDWTuplestore *));
+ /* Ensure the newly allocated slots are NULL, since they will be */
+ /* initialized lazily. */
+ MemSet(afterTriggers->fdw_tuplestores + (afterTriggers->maxquerydepth * sizeof(FDWTuplestore *)), 0,
+ afterTriggers->maxquerydepth * sizeof(FDWTuplestore *));
afterTriggers->maxquerydepth = new_alloc;
}
@@ -3770,6 +3973,7 @@ void
AfterTriggerEndQuery(EState *estate)
{
AfterTriggerEventList *events;
+ FDWTuplestore *fdw_tuplestore;
/* Must be inside a transaction */
Assert(afterTriggers != NULL);
@@ -3810,9 +4014,18 @@ AfterTriggerEndQuery(EState *estate)
break;
}
+ /*
+ * Release the current query FDW tuples store (if any)
+ */
+ fdw_tuplestore = afterTriggers->fdw_tuplestores[afterTriggers->query_depth];
+ if (fdw_tuplestore)
+ {
+ tuplestore_end(fdw_tuplestore->fdwts_tuplestore);
+ pfree(fdw_tuplestore);
+ afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = NULL;
+ }
/* Release query-local storage for events */
afterTriggerFreeEventList(&afterTriggers->query_stack[afterTriggers->query_depth]);
-
afterTriggers->query_depth--;
}
@@ -3900,8 +4113,9 @@ AfterTriggerEndXact(bool isCommit)
* of memory for the list!
*/
if (afterTriggers && afterTriggers->event_cxt)
+ {
MemoryContextDelete(afterTriggers->event_cxt);
-
+ }
afterTriggers = NULL;
}
@@ -3945,7 +4159,6 @@ AfterTriggerBeginSubXact(void)
afterTriggers->firing_stack = (CommandId *)
palloc(DEFTRIG_INITALLOC * sizeof(CommandId));
afterTriggers->maxtransdepth = DEFTRIG_INITALLOC;
-
MemoryContextSwitchTo(old_cxt);
}
else
@@ -4032,6 +4245,15 @@ AfterTriggerEndSubXact(bool isCommit)
*/
while (afterTriggers->query_depth > afterTriggers->depth_stack[my_level])
{
+ FDWTuplestore *fdw_tuplestore = afterTriggers->fdw_tuplestores[afterTriggers->query_depth];
+
+ if (fdw_tuplestore)
+ {
+ tuplestore_end(fdw_tuplestore->fdwts_tuplestore);
+ pfree(fdw_tuplestore);
+ afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = NULL;
+ }
+
afterTriggerFreeEventList(&afterTriggers->query_stack[afterTriggers->query_depth]);
afterTriggers->query_depth--;
}
@@ -4528,9 +4750,11 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
+ char relkind = relinfo->ri_RelationDesc->rd_rel->relkind;
int tgtype_event;
int tgtype_level;
int i;
+ FDWTuplestore *fdw_tuplestore = NULL;
/*
* Check state. We use normal tests not Asserts because it is possible to
@@ -4550,6 +4774,10 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* arrays.
*/
new_event.ate_flags = 0;
+ if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ {
+ new_event.ate_flags = AFTER_TRIGGER_FDW;
+ }
switch (event)
{
case TRIGGER_EVENT_INSERT:
@@ -4558,8 +4786,15 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup == NULL);
Assert(newtup != NULL);
- ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid1));
- ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ {
+ oldtup = newtup;
+ }
+ else
+ {
+ ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ }
}
else
{
@@ -4575,8 +4810,11 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup != NULL);
Assert(newtup == NULL);
- ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
- ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ if (relkind != RELKIND_FOREIGN_TABLE)
+ {
+ ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerSetInvalid(&(new_event.ate_ctid2));
+ }
}
else
{
@@ -4592,9 +4830,12 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
{
Assert(oldtup != NULL);
Assert(newtup != NULL);
- ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
- ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
new_event.ate_flags |= AFTER_TRIGGER_2CTIDS;
+ if (relkind != RELKIND_FOREIGN_TABLE)
+ {
+ ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
+ ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
+ }
}
else
{
@@ -4631,6 +4872,12 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (!TriggerEnabled(estate, relinfo, trigger, event,
modifiedCols, oldtup, newtup))
continue;
+ /* Inject the tupleindex in the event */
+ if (new_event.ate_flags & AFTER_TRIGGER_FDW)
+ {
+ fdw_tuplestore = GetCurrentFDWTuplestore();
+ ((AfterTriggerEventDataFDW *) & new_event)->ate_tupleindex = fdw_tuplestore->fdwts_nextwrite;
+ }
/*
* If the trigger is a foreign key enforcement trigger, there are
@@ -4693,6 +4940,25 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
afterTriggerAddEvent(&afterTriggers->query_stack[afterTriggers->query_depth],
&new_event, &new_shared);
}
+
+ /* Finally, add the tuple to the fdwtuplestore if needed */
+ if (fdw_tuplestore)
+ {
+ int nbtuple = new_event.ate_flags & AFTER_TRIGGER_2CTIDS ? 2 : 1;
+
+ if (fdw_tuplestore->fdwts_nextwrite > INT_MAX - nbtuple)
+ {
+ elog(ERROR, "Cannot insert, update or delete more than %i tuples in "
+ "foreign tables with AFTER row-level trigger", INT_MAX);
+ }
+ tuplestore_puttuple(fdw_tuplestore->fdwts_tuplestore, oldtup);
+ fdw_tuplestore->fdwts_nextwrite += nbtuple;
+ /* For an update trigger, also store the second one. */
+ if (new_event.ate_flags & AFTER_TRIGGER_2CTIDS)
+ {
+ tuplestore_puttuple(fdw_tuplestore->fdwts_tuplestore, newtup);
+ }
+ }
}
Datum
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 6f0f47e..80c6249 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -488,7 +488,7 @@ ldelete:;
(estate->es_processed)++;
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, planSlot);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
@@ -788,7 +788,7 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
+ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, planSlot,
recheckIndexes);
list_free(recheckIndexes);
@@ -986,7 +986,20 @@ ExecModifyTable(ModifyTableState *node)
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
- /* do nothing; FDW must fetch any junk attrs it wants */
+ /*
+ * If the junkAttNo is valid, then it identifies the
+ * wholerow attribute. This is the case when there is an
+ * UPDATE or DELETE trigger.
+ */
+ if (AttributeNumberIsValid(junkfilter->jf_junkAttNo))
+ {
+ datum = ExecGetJunkAttribute(slot,
+ junkfilter->jf_junkAttNo,
+ &isNull);
+ if (isNull)
+ elog(ERROR, "wholerow is NULL");
+ oldtuple = DatumGetHeapTupleHeader(datum);
+ }
}
else
{
@@ -1334,7 +1347,14 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
- /* FDW must fetch any junk attrs it wants */
+ /*
+ * When there is an AFTER trigger, there should be a
+ * wholerow attribute.
+ */
+ AttrNumber junkAttNo = ExecFindJunkAttribute(j, "wholerow");
+
+ if (AttributeNumberIsValid(junkAttNo))
+ j->jf_junkAttNo = junkAttNo;
}
else
{
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 35bda67..52cc5b7 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -240,7 +240,22 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->commandType = parse->commandType;
result->queryId = parse->queryId;
- result->hasReturning = (parse->returningList != NIL);
+
+ /*
+ * Mark the result as having RETURNING only if the returning target list
+ * has non-resjunk entries
+ */
+ result->hasReturning = false;
+ foreach(lp, parse->returningList)
+ {
+ TargetEntry *tle = lfirst(lp);
+
+ if (!tle->resjunk)
+ {
+ result->hasReturning = true;
+ break;
+ }
+ }
result->hasModifyingCTE = parse->hasModifyingCTE;
result->canSetTag = parse->canSetTag;
result->transientPlan = glob->transientPlan;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 0b13645..6489548 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -45,7 +45,8 @@ static Query *rewriteRuleAction(Query *parsetree,
CmdType event,
bool *returning_flag);
static List *adjustJoinTreeList(Query *parsetree, bool removert, int rt_index);
-static void rewriteTargetListIU(Query *parsetree, Relation target_relation,
+static void rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte,
+ Relation target_relation,
List **attrno_list);
static TargetEntry *process_matched_tle(TargetEntry *src_tle,
TargetEntry *prior_tle,
@@ -642,6 +643,10 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
* 4. Sort the tlist into standard order: non-junk fields in order by resno,
* then junk fields (these in no particular order).
*
+ * 5. For an insert on a foreign table with an after trigger, add missing
+ * attribute to the returning targetlist. This is needed to ensure that all
+ * attributes are fetched from the remote side on a returning statement.
+ *
* We must do items 1,2,3 before firing rewrite rules, else rewritten
* references to NEW.foo will produce wrong or incomplete results. Item 4
* is not needed for rewriting, but will be needed by the planner, and we
@@ -653,8 +658,8 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
* processing VALUES RTEs.
*/
static void
-rewriteTargetListIU(Query *parsetree, Relation target_relation,
- List **attrno_list)
+rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte,
+ Relation target_relation, List **attrno_list)
{
CmdType commandType = parsetree->commandType;
TargetEntry **new_tles;
@@ -730,6 +735,28 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
}
}
+ /*
+ * For foreign tables, force RETURNING the whole-row if a corresponding
+ * AFTER trigger is found
+ */
+ if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE &&
+ target_relation->trigdesc &&
+ ((commandType == CMD_INSERT && target_relation->trigdesc->trig_insert_after_row) ||
+ (commandType == CMD_UPDATE && target_relation->trigdesc->trig_update_after_row)))
+
+ {
+ Var *var = makeWholeRowVar(target_rte,
+ parsetree->resultRelation,
+ 0,
+ false);
+ TargetEntry *tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->returningList) + 1,
+ "wholerow",
+ true);
+
+ parsetree->returningList = lappend(parsetree->returningList, tle);
+ }
+
for (attrno = 1; attrno <= numattrs; attrno++)
{
TargetEntry *new_tle = new_tles[attrno - 1];
@@ -818,7 +845,6 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
}
pfree(new_tles);
-
parsetree->targetList = list_concat(new_tlist, junk_tlist);
}
@@ -1174,7 +1200,7 @@ static void
rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
Relation target_relation)
{
- Var *var;
+ Var *var = NULL;
const char *attrname;
TargetEntry *tle;
@@ -1206,7 +1232,27 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
target_relation);
- return;
+ /*
+ * If we have a trigger corresponding to the operation, add a wholerow
+ * attribute.
+ */
+ if (target_relation->trigdesc &&
+ ((parsetree->commandType == CMD_UPDATE &&
+ (target_relation->trigdesc->trig_update_after_row
+ || target_relation->trigdesc->trig_update_before_row)) ||
+ (parsetree->commandType == CMD_DELETE &&
+ (target_relation->trigdesc->trig_delete_after_row ||
+ target_relation->trigdesc->trig_delete_before_row))))
+ {
+ var = makeWholeRowVar(target_rte,
+ parsetree->resultRelation,
+ 0,
+ false);
+
+ attrname = "wholerow";
+
+ }
+
}
else
{
@@ -1221,13 +1267,15 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
attrname = "wholerow";
}
+ if (var != NULL)
+ {
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->targetList) + 1,
+ pstrdup(attrname),
+ true);
- tle = makeTargetEntry((Expr *) var,
- list_length(parsetree->targetList) + 1,
- pstrdup(attrname),
- true);
-
- parsetree->targetList = lappend(parsetree->targetList, tle);
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+ }
}
@@ -2965,19 +3013,19 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
List *attrnos;
/* Process the main targetlist ... */
- rewriteTargetListIU(parsetree, rt_entry_relation, &attrnos);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, &attrnos);
/* ... and the VALUES expression lists */
rewriteValuesRTE(values_rte, rt_entry_relation, attrnos);
}
else
{
/* Process just the main targetlist */
- rewriteTargetListIU(parsetree, rt_entry_relation, NULL);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL);
}
}
else if (event == CMD_UPDATE)
{
- rewriteTargetListIU(parsetree, rt_entry_relation, NULL);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL);
rewriteTargetListUD(parsetree, rt_entry, rt_entry_relation);
}
else if (event == CMD_DELETE)
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 44d686c..ed598f0 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -150,7 +150,8 @@ extern bool ExecBRDeleteTriggers(EState *estate,
ItemPointer tupleid);
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ TupleTableSlot *slot);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -167,6 +168,7 @@ extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple newtuple,
+ TupleTableSlot *slot,
List *recheckIndexes);
extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 60506e0..3405b6c 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -1158,6 +1158,36 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
ERROR: must be owner of foreign server s9
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+ERROR: "foreign_table_1" is a foreign table
+DETAIL: Foreign Tables cannot have constraint triggers.
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+DROP FUNCTION dummy_trigger();
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
ERROR: foreign table "no_table" does not exist
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index f819eb1..6166097 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -470,6 +470,43 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest_before_stmt;
+
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+
+DROP FUNCTION dummy_trigger();
+
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
DROP FOREIGN TABLE IF EXISTS no_table;
Hello.
Did you have time to review the latest version of this patch ? Is there
anything I can do to get this "ready for commiter" ?
Thank you for all the work performed so far.
Le mardi 4 février 2014 13:16:22 Ronan Dunklau a écrit :
Le lundi 3 février 2014 23:28:45 Noah Misch a écrit :
On Sun, Feb 02, 2014 at 11:53:51AM +0100, Ronan Dunklau wrote:
Le jeudi 30 janvier 2014 14:05:08 Noah Misch a écrit :
On Thu, Jan 23, 2014 at 03:17:35PM +0100, Ronan Dunklau wrote:
What do you think about this approach ? Is there something I missed
which
would make it not sustainable ?Seems basically reasonable. I foresee multiple advantages from having
one
tuplestore per query level as opposed to one for the entire
transaction.
You would remove the performance trap of backing up the tuplestore by
rescanning. It permits reclaiming memory and disk space in
AfterTriggerEndQuery() rather than at end of transaction. You could
remove
ate_ptr1 and ate_ptr2 from AfterTriggerEventDataFDW and just store the
flags word: depending on AFTER_TRIGGER_2CTIDS, grab either the next
one
or
the next two tuples from the tuplestore. Using work_mem per
AfterTriggerBeginQuery() instead of per transaction is no problem.
What
do
you think of that design change?I agree that this design is better, but I have some objections.
We can remove ate_ptr2 and rely on the AFTER_TRIGGER_2CTIDS flag, but
the
rescanning and ate_ptr1 (renamed ate_tupleindex in the attached patch)
can't go away.Consider for example the case of a foreign table with more than one
AFTER
UPDATE triggers. Unless we store the tuples once for each trigger, we
will
have to rescan the tuplestore.Will we? Within a given query level, when do (non-deferred) triggers
execute in an order other than the enqueue order?Let me explain what I had in mind.
Looking at the code in AfterTriggerSaveEvent:
- we build a "template" AfterTriggerEvent, and store the tuple(s)
- for each suitable after trigger that matches the trigger type, as well as
the WHEN condition if any, a copy of the previously built AfterTriggerEvent
is queuedLater, those events are fired in order.
This means that more than one event can be fired for one tuple.
Take this example:
CREATE TRIGGER trig_row_after1
AFTER UPDATE ON rem2
FOR EACH ROW
WHEN (NEW.f1 % 5 < 3)
EXECUTE PROCEDURE trigger_func('TRIG1');CREATE TRIGGER trig_row_after2
AFTER UPDATE ON rem2
FOR EACH ROW
WHEN (NEW.f1 % 5 < 4)
EXECUTE PROCEDURE trigger_func('TRIG2');UPDATE rem2 set f2 = 'something';
Assuming 5 rows with f1 as a serial, the fired AfterTriggerEvent's
ate_tupleindex will be, in that order. Ass0-0-2-2-4-8-8
So, at least a backward seek is required for trig_row_after2 to be able to
retrieve a tuple that was already consumed when firing trig_row_after1.On a side note, this made me realize that it is better to avoid storing a
tuple entirely if there is no enabled trigger (the f1 = 4 case above). The
attached patch does that, so the previous sequence becomes:0-0-2-2-4-6-6
It also prevents from initalizing a tuplestore at all if its not needed.
To mitigate the effects of this behaviour, I added the option to perform
a
reverse_seek when the looked-up tuple is nearer from the current index
than
from the start.If there's still a need to seek within the tuplestore, that should get rid
of the O(n^2) effect. I'm hoping that per-query-level tuplestores will
eliminate the need to seek entirely.I think the only case when seeking is still needed is when there are more
than one after trigger that need to be fired, since the abovementioned
change prevents from seeking to skip tuples.If you do pursue that change, make sure the code still does the right
thing
when it drops queued entries during subxact abort.I don't really understand what should be done at that stage. Since
triggers on foreign tables are not allowed to be deferred, everything
should be cleaned up at the end of each query, right ? So, there
shouldn't be any queued entries.I suspect that's right. If you haven't looked over
AfterTriggerEndSubXact(), please do so and ensure all its actions still
make sense in the context of this new kind of trigger storage.You're right, I missed something here. When aborting a subxact, the
tuplestores for queries below the subxact query depth should be cleaned, if
any, because AfterTriggerEndQuery has not been called for the failing query.The attached patch fixes that.
The attached patch checks this, and add documentation for this
limitation.
I'm not really sure about how to phrase that correctly in the error
message
and the documentation. One can store at most INT_MAX foreign tuples,
which
means that at most INT_MAX insert or delete or "half-updates" can
occur.
By
half-updates, I mean that for update two tuples are stored whereas
in
contrast to only one for insert and delete trigger.Besides, I don't know where this disclaimer should be in the
documentation.
Any advice here ?I wouldn't mention that limitation.
Maybe it shouldn't be so prominent, but I still think a note somewhere
couldn't hurt.Perhaps. There's not much documentation of such implementation upper
limits, and there's no usage of "INT_MAX" outside of parts that discuss
writing C code. I'm not much of a visionary when it comes to the
documentation; I try to document new things with an amount of detail
similar to old features.Ok, I removed the paragraph documenting the limitation.
Should the use of work_mem be documented somewhere, too ?
I wouldn't. Most uses of work_mem are undocumented, even relatively major
ones like count(DISTINCT ...) and CTEs. So, while I'd generally favor a
patch documenting all/most of the things that use work_mem, it would be
odd
to document one new consumer apart from the others.Ok.
This is the performance trap I mentioned above; it brings potential
O(n^2)
complexity to certain AFTER trigger execution scenarios.What scenarios do you have in mind ? I "fixed" the problem when there
are
multiple triggers on a foreign table, do you have any other one ?I'm not aware of such a performance trap in your latest design.
Good !
Thanks,
nm
--
Ronan Dunklau
http://dalibo.com - http://dalibo.org
I tried to check the latest (v8) patch again, then could not find
problem in your design change from the v7.
As Noah pointed out, it uses per query-depth tuplestore being released
on AfterTriggerEndSubXact.
So, may I mark it as "ready for committer"?
2014-03-03 15:48 GMT+09:00 Ronan Dunklau <ronan.dunklau@dalibo.com>:
Hello.
Did you have time to review the latest version of this patch ? Is there
anything I can do to get this "ready for commiter" ?Thank you for all the work performed so far.
Le mardi 4 février 2014 13:16:22 Ronan Dunklau a écrit :
Le lundi 3 février 2014 23:28:45 Noah Misch a écrit :
On Sun, Feb 02, 2014 at 11:53:51AM +0100, Ronan Dunklau wrote:
Le jeudi 30 janvier 2014 14:05:08 Noah Misch a écrit :
On Thu, Jan 23, 2014 at 03:17:35PM +0100, Ronan Dunklau wrote:
What do you think about this approach ? Is there something I missed
which
would make it not sustainable ?Seems basically reasonable. I foresee multiple advantages from having
one
tuplestore per query level as opposed to one for the entire
transaction.
You would remove the performance trap of backing up the tuplestore by
rescanning. It permits reclaiming memory and disk space in
AfterTriggerEndQuery() rather than at end of transaction. You could
remove
ate_ptr1 and ate_ptr2 from AfterTriggerEventDataFDW and just store the
flags word: depending on AFTER_TRIGGER_2CTIDS, grab either the next
one
or
the next two tuples from the tuplestore. Using work_mem per
AfterTriggerBeginQuery() instead of per transaction is no problem.
What
do
you think of that design change?I agree that this design is better, but I have some objections.
We can remove ate_ptr2 and rely on the AFTER_TRIGGER_2CTIDS flag, but
the
rescanning and ate_ptr1 (renamed ate_tupleindex in the attached patch)
can't go away.Consider for example the case of a foreign table with more than one
AFTER
UPDATE triggers. Unless we store the tuples once for each trigger, we
will
have to rescan the tuplestore.Will we? Within a given query level, when do (non-deferred) triggers
execute in an order other than the enqueue order?Let me explain what I had in mind.
Looking at the code in AfterTriggerSaveEvent:
- we build a "template" AfterTriggerEvent, and store the tuple(s)
- for each suitable after trigger that matches the trigger type, as well as
the WHEN condition if any, a copy of the previously built AfterTriggerEvent
is queuedLater, those events are fired in order.
This means that more than one event can be fired for one tuple.
Take this example:
CREATE TRIGGER trig_row_after1
AFTER UPDATE ON rem2
FOR EACH ROW
WHEN (NEW.f1 % 5 < 3)
EXECUTE PROCEDURE trigger_func('TRIG1');CREATE TRIGGER trig_row_after2
AFTER UPDATE ON rem2
FOR EACH ROW
WHEN (NEW.f1 % 5 < 4)
EXECUTE PROCEDURE trigger_func('TRIG2');UPDATE rem2 set f2 = 'something';
Assuming 5 rows with f1 as a serial, the fired AfterTriggerEvent's
ate_tupleindex will be, in that order. Ass0-0-2-2-4-8-8
So, at least a backward seek is required for trig_row_after2 to be able to
retrieve a tuple that was already consumed when firing trig_row_after1.On a side note, this made me realize that it is better to avoid storing a
tuple entirely if there is no enabled trigger (the f1 = 4 case above). The
attached patch does that, so the previous sequence becomes:0-0-2-2-4-6-6
It also prevents from initalizing a tuplestore at all if its not needed.
To mitigate the effects of this behaviour, I added the option to perform
a
reverse_seek when the looked-up tuple is nearer from the current index
than
from the start.If there's still a need to seek within the tuplestore, that should get rid
of the O(n^2) effect. I'm hoping that per-query-level tuplestores will
eliminate the need to seek entirely.I think the only case when seeking is still needed is when there are more
than one after trigger that need to be fired, since the abovementioned
change prevents from seeking to skip tuples.If you do pursue that change, make sure the code still does the right
thing
when it drops queued entries during subxact abort.I don't really understand what should be done at that stage. Since
triggers on foreign tables are not allowed to be deferred, everything
should be cleaned up at the end of each query, right ? So, there
shouldn't be any queued entries.I suspect that's right. If you haven't looked over
AfterTriggerEndSubXact(), please do so and ensure all its actions still
make sense in the context of this new kind of trigger storage.You're right, I missed something here. When aborting a subxact, the
tuplestores for queries below the subxact query depth should be cleaned, if
any, because AfterTriggerEndQuery has not been called for the failing query.The attached patch fixes that.
The attached patch checks this, and add documentation for this
limitation.
I'm not really sure about how to phrase that correctly in the error
message
and the documentation. One can store at most INT_MAX foreign tuples,
which
means that at most INT_MAX insert or delete or "half-updates" can
occur.
By
half-updates, I mean that for update two tuples are stored whereas
in
contrast to only one for insert and delete trigger.Besides, I don't know where this disclaimer should be in the
documentation.
Any advice here ?I wouldn't mention that limitation.
Maybe it shouldn't be so prominent, but I still think a note somewhere
couldn't hurt.Perhaps. There's not much documentation of such implementation upper
limits, and there's no usage of "INT_MAX" outside of parts that discuss
writing C code. I'm not much of a visionary when it comes to the
documentation; I try to document new things with an amount of detail
similar to old features.Ok, I removed the paragraph documenting the limitation.
Should the use of work_mem be documented somewhere, too ?
I wouldn't. Most uses of work_mem are undocumented, even relatively major
ones like count(DISTINCT ...) and CTEs. So, while I'd generally favor a
patch documenting all/most of the things that use work_mem, it would be
odd
to document one new consumer apart from the others.Ok.
This is the performance trap I mentioned above; it brings potential
O(n^2)
complexity to certain AFTER trigger execution scenarios.What scenarios do you have in mind ? I "fixed" the problem when there
are
multiple triggers on a foreign table, do you have any other one ?I'm not aware of such a performance trap in your latest design.
Good !
Thanks,
nm--
Ronan Dunklau
http://dalibo.com - http://dalibo.org
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Mar 03, 2014 at 11:10:30PM +0900, Kohei KaiGai wrote:
I tried to check the latest (v8) patch again, then could not find
problem in your design change from the v7.
As Noah pointed out, it uses per query-depth tuplestore being released
on AfterTriggerEndSubXact.So, may I mark it as "ready for committer"?
Yes. Re-reviewing this is next on my list.
--
Noah Misch
EnterpriseDB http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
This version looks basically good. I have some cosmetic things to sweep up
before commit. One point is a bit more substantial:
On Tue, Feb 04, 2014 at 01:16:22PM +0100, Ronan Dunklau wrote:
Le lundi 3 f�vrier 2014 23:28:45 Noah Misch a �crit :
On Sun, Feb 02, 2014 at 11:53:51AM +0100, Ronan Dunklau wrote:
We can remove ate_ptr2 and rely on the AFTER_TRIGGER_2CTIDS flag, but the
rescanning and ate_ptr1 (renamed ate_tupleindex in the attached patch)
can't go away.Consider for example the case of a foreign table with more than one AFTER
UPDATE triggers. Unless we store the tuples once for each trigger, we will
have to rescan the tuplestore.Will we? Within a given query level, when do (non-deferred) triggers
execute in an order other than the enqueue order?Let me explain what I had in mind.
Looking at the code in AfterTriggerSaveEvent:
- we build a "template" AfterTriggerEvent, and store the tuple(s)
- for each suitable after trigger that matches the trigger type, as well as
the WHEN condition if any, a copy of the previously built AfterTriggerEvent is
queuedLater, those events are fired in order.
This means that more than one event can be fired for one tuple.
Take this example:
[snip]
Thanks; that illuminated the facts I was missing.
On a side note, this made me realize that it is better to avoid storing a
tuple entirely if there is no enabled trigger (the f1 = 4 case above). The
attached patch does that, so the previous sequence becomes:0-0-2-2-4-6-6
It also prevents from initalizing a tuplestore at all if its not needed.
That's a sensible improvement.
To mitigate the effects of this behaviour, I added the option to perform a
reverse_seek when the looked-up tuple is nearer from the current index
than
from the start.If there's still a need to seek within the tuplestore, that should get rid
of the O(n^2) effect. I'm hoping that per-query-level tuplestores will
eliminate the need to seek entirely.I think the only case when seeking is still needed is when there are more than
one after trigger that need to be fired, since the abovementioned change
prevents from seeking to skip tuples.
Agreed. More specifically, I see only two scenarios for retrieving tuples
from the tuplestore. Scenario one is that we need the next tuple (or pair of
tuples, depending on the TriggerEvent). Scenario two is that we need the
tuple(s) most recently retrieved. If that's correct, I'm inclined to
rearrange afterTriggerInvokeEvents() and AfterTriggerExecute() to remember the
tuple or pair of tuples most recently retrieved. They'll then never call
tuplestore_advance() just to reposition. Do you see a problem with that?
I was again somewhat tempted to remove ate_tupleindex, perhaps by defining the
four flag bits this way:
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* two bits describing the size of and tuple sources for this event */
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
#define AFTER_TRIGGER_FDW_FETCH 0x40000000
#define AFTER_TRIGGER_1CTID 0x80000000
#define AFTER_TRIGGER_2CTID 0xC0000000
AFTER_TRIGGER_FDW_FETCH and AFTER_TRIGGER_FDW_REUSE correspond to the
aforementioned scenarios one and two, respectively. I think, though, I'll
rate this as needless micro-optimization and not bother; opinions welcome.
(The savings is four bytes per foreign table trigger event.)
Thanks,
nm
--
Noah Misch
EnterpriseDB http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Le mercredi 5 mars 2014 22:36:44 Noah Misch a écrit :
Agreed. More specifically, I see only two scenarios for retrieving tuples
from the tuplestore. Scenario one is that we need the next tuple (or pair
of tuples, depending on the TriggerEvent). Scenario two is that we need
the tuple(s) most recently retrieved. If that's correct, I'm inclined to
rearrange afterTriggerInvokeEvents() and AfterTriggerExecute() to remember
the tuple or pair of tuples most recently retrieved. They'll then never
call tuplestore_advance() just to reposition. Do you see a problem with
that?
I don't see any problem with that. I don't know how this would be implemented,
but it would make sense to avoid those scans, as long as a fresh copy is
passed to the trigger: modifications to a tuple performed in an after trigger
should not be visible to the next one.
I was again somewhat tempted to remove ate_tupleindex, perhaps by defining
the four flag bits this way:#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* two bits describing the size of and tuple sources for this event */
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
#define AFTER_TRIGGER_FDW_FETCH 0x40000000
#define AFTER_TRIGGER_1CTID 0x80000000
#define AFTER_TRIGGER_2CTID 0xC0000000AFTER_TRIGGER_FDW_FETCH and AFTER_TRIGGER_FDW_REUSE correspond to the
aforementioned scenarios one and two, respectively. I think, though, I'll
rate this as needless micro-optimization and not bother; opinions welcome.
(The savings is four bytes per foreign table trigger event.)
I was already happy with having a lower footprint for foreign table trigger
events than for regular trigger events, but if we remove the need for seeking
in the tuplestore entirely, it would make sense to get rid of the index.
Thanks,
nm
Thanks to you.
--
Ronan Dunklau
http://dalibo.com - http://dalibo.org
I hacked on this for awhile, but there remain two matters on which I'm
uncertain about the right way forward.
(1) To acquire the old tuple for UPDATE/DELETE operations, the patch closely
parallels our handling for INSTEAD OF triggers on views. It adds a wholerow
resjunk attribute, from which it constructs a HeapTuple before calling a
trigger function. This loses the system columns, an irrelevant consideration
for views but meaningful for foreign tables. postgres_fdw maintains the
"ctid" system column (t_self), but triggers will always see an invalid t_self
for the old tuple. One way to fix this is to pass around the old tuple data
as ROW(ctid, oid, xmin, cmin, xmax, cmax, tableoid, wholerow). That's fairly
close to sufficient, but it doesn't cover t_ctid. Frankly, I would like to
find a principled excuse to not worry about making foreign table system
columns accessible from triggers. Supporting system columns dramatically
affects the mechanism, and what trigger is likely to care? Unfortunately,
that argument seems too weak. Does anyone have a cleaner idea for keeping
track of the system column values or a stronger argument for not bothering?
(2) When a foreign table has an AFTER ROW trigger, the FDW's
ExecForeign{Insert,Update,Delete} callbacks must return a slot covering all
columns. Current FDW API documentation says things like this:
The data in the returned slot is used only if the INSERT query has a
RETURNING clause. Hence, the FDW could choose to optimize away returning
some or all columns depending on the contents of the RETURNING clause.
Consistent with that, postgres_fdw inspects the returningLists of the
ModifyTable node to decide which columns are necessary. This patch has
rewriteTargetListIU() add a resjunk wholerow var to the returningList of any
query that will fire an AFTER ROW trigger on a foreign table. That avoids the
need to change the FDW API contract or any postgres_fdw code. I was pleased
about that for a time, but on further review, I'm doubting that the benefit
for writable FDWs justifies muddying the definition of returningList; until
now, returningList has been free from resjunk TLEs. For example, callers of
FetchStatementTargetList() may be unprepared to see an all-resjunk list,
instead of NIL, for a data modification statement that returns nothing.
If we do keep the patch's approach, I'm inclined to rename returningList.
However, I more lean toward adding a separate flag to indicate the need to
return a complete tuple regardless of the RETURNING list. The benefits of
overloading returningList are all short-term benefits. We know that the FDW
API is still converging, so changing it seems eventually-preferable to, and
safer than, changing the name or meaning of returningList. Thoughts?
On Thu, Mar 06, 2014 at 09:11:19AM +0100, Ronan Dunklau wrote:
Le mercredi 5 mars 2014 22:36:44 Noah Misch a �crit :
Agreed. More specifically, I see only two scenarios for retrieving tuples
from the tuplestore. Scenario one is that we need the next tuple (or pair
of tuples, depending on the TriggerEvent). Scenario two is that we need
the tuple(s) most recently retrieved. If that's correct, I'm inclined to
rearrange afterTriggerInvokeEvents() and AfterTriggerExecute() to remember
the tuple or pair of tuples most recently retrieved. They'll then never
call tuplestore_advance() just to reposition. Do you see a problem with
that?I don't see any problem with that. I don't know how this would be implemented,
but it would make sense to avoid those scans, as long as a fresh copy is
passed to the trigger: modifications to a tuple performed in an after trigger
should not be visible to the next one.
Trigger functions are not permitted to modify tg_trigtuple or tg_newtuple;
notice that, for non-foreign triggers, we pass shared_buffers-backed tuples in
those fields. Therefore, no copy is necessary.
I was again somewhat tempted to remove ate_tupleindex, perhaps by defining
the four flag bits this way:#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* two bits describing the size of and tuple sources for this event */
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
#define AFTER_TRIGGER_FDW_FETCH 0x40000000
#define AFTER_TRIGGER_1CTID 0x80000000
#define AFTER_TRIGGER_2CTID 0xC0000000AFTER_TRIGGER_FDW_FETCH and AFTER_TRIGGER_FDW_REUSE correspond to the
aforementioned scenarios one and two, respectively. I think, though, I'll
rate this as needless micro-optimization and not bother; opinions welcome.
(The savings is four bytes per foreign table trigger event.)I was already happy with having a lower footprint for foreign table trigger
events than for regular trigger events, but if we remove the need for seeking
in the tuplestore entirely, it would make sense to get rid of the index.
I'm pleased with how this turned out. Besides the memory savings in question,
this removed the INT_MAX limitation and simplified the code overall. I did
not observe a notable runtime improvement, though that's unsurprising.
Other notable changes in the attached revision:
1. UPDATE/DELETE row-level triggers on foreign tables and INSTEAD OF triggers
on views have a similar requirement to generate a HeapTuple representing the
old row. View triggers did so in nodeModifyTable.c, while foreign table
triggers did so in trigger.c. Both were valid choices, but the code siting
should not be relkind-dependent without good reason. I centralized this in
ExecModifyTable().
2. Made CREATE TRIGGER forbid INSTEAD OF and TRUNCATE triggers on foreign
tables. The documentation already claimed they were unavailable.
3. Fixed pointer arithmetic in AfterTriggerBeginQuery()'s MemSet() call.
4. Modified GetCurrentFDWTuplestore() to allocate the tuplestore in
TopTransactionContext. We explicitly put the events list there (specifically,
in a documentation-only child of that context), so it seemed more consistent
to do the same for the associated foreign tuples. I did not find any live bug
from the previous coding, because CurrentMemoryContext always happened to be
one that survived past AfterTriggerEndQuery().
5. Updated comments and documentation that still reflected earlier versions of
the patch, as well as older comments obsoleted by the patch.
6. Reverted cosmetic changes, like addition of braces and blank lines, to
passages of code not otherwise changing. Please see:
https://wiki.postgresql.org/wiki/Creating_Clean_Patches
--
Noah Misch
EnterpriseDB http://www.enterprisedb.com
Attachments:
foreign_trigger_v9.patchtext/plain; charset=us-asciiDownload
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 9a3d651..4390a8e 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2496,3 +2496,322 @@ select * from rem1;
11 | bye remote
(4 rows)
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %',
+ TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1
+ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1
+ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %',
+ tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+delete from rem1;
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (1,hi)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (10,"hi remote")
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,bye)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (11,"bye remote")
+NOTICE: trigger_func(<NULL>) called: action = DELETE, when = AFTER, level = STATEMENT
+insert into rem1 values(1,'insert');
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (1,insert)
+NOTICE: trigger_func(<NULL>) called: action = INSERT, when = AFTER, level = STATEMENT
+update rem1 set f2 = 'update' where f1 = 1;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,insert),NEW: (1,update)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+update rem1 set f2 = f2 || f2;
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (1,update),NEW: (1,updateupdate)
+NOTICE: trigger_func(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+DELETE from rem1;
+-- Test WHEN conditions
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (2,update)
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (2,update),NEW: (2,"update update")
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+NOTICE: trig_row_before_delete(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+NOTICE: trig_row_after_delete(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE: OLD: (2,"update update")
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+-- Test various RETURN statements in BEFORE triggers.
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------
+ insert triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | insert triggered !
+ 2 | insert triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------
+ 1 | triggered !
+ 2 | triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------
+ skidoo triggered !
+ skidoo triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------
+ 1 | skidoo triggered !
+ 2 | skidoo triggered !
+(2 rows)
+
+DELETE FROM rem1;
+-- Add a second trigger, to check that the changes are propagated correctly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+(1 row)
+
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+ f2
+--------------------------------
+ insert triggered ! triggered !
+(1 row)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | insert triggered ! triggered !
+ 2 | insert triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------
+ 1 | triggered ! triggered !
+ 2 | triggered ! triggered !
+(2 rows)
+
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+ f2
+--------------------------------
+ skidoo triggered ! triggered !
+ skidoo triggered ! triggered !
+(2 rows)
+
+SELECT * from loc1;
+ f1 | f2
+----+--------------------------------
+ 1 | skidoo triggered ! triggered !
+ 2 | skidoo triggered ! triggered !
+(2 rows)
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+DELETE from rem1;
+INSERT INTO rem1 VALUES (1, 'test');
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+UPDATE rem1 SET f2 = 'test2';
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DELETE from rem1;
+SELECT * from loc1;
+ f1 | f2
+----+------
+ 1 | test
+(1 row)
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+INSERT INTO rem1(f2) VALUES ('test');
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (12,test)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (12,"test triggered !")
+UPDATE rem1 SET f2 = 'testo';
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,testo)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE: OLD: (12,"test triggered !"),NEW: (12,"testo triggered !")
+-- Test returning system attributes
+INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax;
+NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE: NEW: (13,test)
+NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE: NEW: (13,"test triggered !")
+ ctid | xmin | xmax
+--------+------+------------
+ (0,27) | 180 | 4294967295
+(1 row)
+
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 21b15ca..3d2ee73 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -390,3 +390,219 @@ insert into loc1(f2) values('bye');
insert into rem1(f2) values('bye remote');
select * from loc1;
select * from rem1;
+
+-- ===================================================================
+-- test local triggers
+-- ===================================================================
+
+-- Trigger functions "borrowed" from triggers regress test.
+CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE NOTICE 'trigger_func(%) called: action = %, when = %, level = %',
+ TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+ RETURN NULL;
+END;$$;
+
+CREATE TRIGGER trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1
+ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1
+ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+
+CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
+LANGUAGE plpgsql AS $$
+
+declare
+ oldnew text[];
+ relid text;
+ argstr text;
+begin
+
+ relid := TG_relid::regclass;
+ argstr := '';
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ RAISE NOTICE '%(%) % % % ON %',
+ tg_name, argstr, TG_when, TG_level, TG_OP, relid;
+ oldnew := '{}'::text[];
+ if TG_OP != 'INSERT' then
+ oldnew := array_append(oldnew, format('OLD: %s', OLD));
+ end if;
+
+ if TG_OP != 'DELETE' then
+ oldnew := array_append(oldnew, format('NEW: %s', NEW));
+ end if;
+
+ RAISE NOTICE '%', array_to_string(oldnew, ',');
+
+ if TG_OP = 'DELETE' then
+ return OLD;
+ else
+ return NEW;
+ end if;
+end;
+$$;
+
+-- Test basic functionality
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+delete from rem1;
+insert into rem1 values(1,'insert');
+update rem1 set f2 = 'update' where f1 = 1;
+update rem1 set f2 = f2 || f2;
+
+
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+
+DELETE from rem1;
+
+
+-- Test WHEN conditions
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+
+-- Insert or update not matching: triggers are fired
+INSERT INTO rem1 values(2, 'update');
+UPDATE rem1 set f2 = 'update update' where f1 = '2';
+
+CREATE TRIGGER trig_row_before_delete
+BEFORE DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after_delete
+AFTER DELETE ON rem1
+FOR EACH ROW
+WHEN (OLD.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+-- Trigger is fired for f1=2, not for f1=1
+DELETE FROM rem1;
+
+-- cleanup
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_after_insupd ON rem1;
+DROP TRIGGER trig_row_before_delete ON rem1;
+DROP TRIGGER trig_row_after_delete ON rem1;
+
+
+-- Test various RETURN statements in BEFORE triggers.
+
+CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.f2 := NEW.f2 || ' triggered !';
+ RETURN NEW;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+-- The new values should have 'triggered' appended
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DELETE FROM rem1;
+
+-- Add a second trigger, to check that the changes are propagated correctly
+-- from trigger to trigger
+CREATE TRIGGER trig_row_before_insupd2
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1 values(1, 'insert');
+SELECT * from loc1;
+INSERT INTO rem1 values(2, 'insert') RETURNING f2;
+SELECT * from loc1;
+UPDATE rem1 set f2 = '';
+SELECT * from loc1;
+UPDATE rem1 set f2 = 'skidoo' RETURNING f2;
+SELECT * from loc1;
+
+DROP TRIGGER trig_row_before_insupd ON rem1;
+DROP TRIGGER trig_row_before_insupd2 ON rem1;
+
+DELETE from rem1;
+
+INSERT INTO rem1 VALUES (1, 'test');
+
+-- Test with a trigger returning NULL
+CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trig_null
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trig_null();
+
+-- Nothing should have changed.
+INSERT INTO rem1 VALUES (2, 'test2');
+
+SELECT * from loc1;
+
+UPDATE rem1 SET f2 = 'test2';
+
+SELECT * from loc1;
+
+DELETE from rem1;
+
+SELECT * from loc1;
+
+DROP TRIGGER trig_null ON rem1;
+DELETE from rem1;
+
+-- Test a combination of local and remote triggers
+CREATE TRIGGER trig_row_before
+BEFORE INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_row_after
+AFTER INSERT OR UPDATE OR DELETE ON rem1
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1
+FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate();
+
+INSERT INTO rem1(f2) VALUES ('test');
+UPDATE rem1 SET f2 = 'testo';
+
+-- Test returning system attributes
+INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 6c06f1a..abaaa6b 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -308,7 +308,8 @@ AddForeignUpdateTargets (Query *parsetree,
extra values to be fetched. Each such entry must be marked
<structfield>resjunk</> = <literal>true</>, and must have a distinct
<structfield>resname</> that will identify it at execution time.
- Avoid using names matching <literal>ctid<replaceable>N</></literal> or
+ Avoid using names matching <literal>ctid<replaceable>N</></literal>,
+ <literal>wholerow</literal>, or
<literal>wholerow<replaceable>N</></literal>, as the core system can
generate junk columns of these names.
</para>
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index a8fba49..d270d66 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -43,9 +43,10 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<para>
<command>CREATE TRIGGER</command> creates a new trigger. The
- trigger will be associated with the specified table or view and will
- execute the specified function <replaceable
- class="parameter">function_name</replaceable> when certain events occur.
+ trigger will be associated with the specified table, view, or foreign table
+ and will execute the specified
+ function <replaceable class="parameter">function_name</replaceable> when
+ certain events occur.
</para>
<para>
@@ -93,7 +94,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<para>
The following table summarizes which types of triggers may be used on
- tables and views:
+ tables, views, and foreign tables:
</para>
<informaltable id="supported-trigger-types">
@@ -110,8 +111,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>BEFORE</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables and foreign tables</entry>
+ <entry align="center">Tables, views, and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -121,8 +122,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<row>
<entry align="center" morerows="1"><literal>AFTER</></entry>
<entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
- <entry align="center">Tables</entry>
- <entry align="center">Tables and views</entry>
+ <entry align="center">Tables and foreign tables</entry>
+ <entry align="center">Tables, views, and foreign tables</entry>
</row>
<row>
<entry align="center"><command>TRUNCATE</></entry>
@@ -164,13 +165,13 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<firstterm>constraint trigger</>. This is the same as a regular trigger
except that the timing of the trigger firing can be adjusted using
<xref linkend="SQL-SET-CONSTRAINTS">.
- Constraint triggers must be <literal>AFTER ROW</> triggers. They can
- be fired either at the end of the statement causing the triggering event,
- or at the end of the containing transaction; in the latter case they are
- said to be <firstterm>deferred</>. A pending deferred-trigger firing can
- also be forced to happen immediately by using <command>SET CONSTRAINTS</>.
- Constraint triggers are expected to raise an exception when the constraints
- they implement are violated.
+ Constraint triggers must be <literal>AFTER ROW</> triggers on tables. They
+ can be fired either at the end of the statement causing the triggering
+ event, or at the end of the containing transaction; in the latter case they
+ are said to be <firstterm>deferred</>. A pending deferred-trigger firing
+ can also be forced to happen immediately by using <command>SET
+ CONSTRAINTS</>. Constraint triggers are expected to raise an exception
+ when the constraints they implement are violated.
</para>
<para>
@@ -244,8 +245,8 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
<term><replaceable class="parameter">table_name</replaceable></term>
<listitem>
<para>
- The name (optionally schema-qualified) of the table or view the trigger
- is for.
+ The name (optionally schema-qualified) of the table, view, or foreign
+ table the trigger is for.
</para>
</listitem>
</varlistentry>
@@ -481,6 +482,14 @@ CREATE TRIGGER view_insert
<refsect1 id="SQL-CREATETRIGGER-compatibility">
<title>Compatibility</title>
+ <!--
+ It's not clear whether SQL/MED contemplates triggers on foreign tables.
+ Its <drop basic column definition> General Rules do mention the possibility
+ of a reference from a trigger column list. On the other hand, nothing
+ overrides the fact that CREATE TRIGGER only targets base tables. For now,
+ do not document the compatibility status of triggers on foreign tables.
+ -->
+
<para>
The <command>CREATE TRIGGER</command> statement in
<productname>PostgreSQL</productname> implements a subset of the
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index f579340..7ed91dd 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -33,20 +33,21 @@
<para>
A trigger is a specification that the database should automatically
execute a particular function whenever a certain type of operation is
- performed. Triggers can be attached to both tables and views.
+ performed. Triggers can be attached to tables, views, and foreign tables.
</para>
<para>
- On tables, triggers can be defined to execute either before or after any
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> operation, either once per modified row,
+ On tables and foreign tables, triggers can be defined to execute either
+ before or after any <command>INSERT</command>, <command>UPDATE</command>,
+ or <command>DELETE</command> operation, either once per modified row,
or once per <acronym>SQL</acronym> statement.
<command>UPDATE</command> triggers can moreover be set to fire only if
certain columns are mentioned in the <literal>SET</literal> clause of the
<command>UPDATE</command> statement.
Triggers can also fire for <command>TRUNCATE</command> statements.
If a trigger event occurs, the trigger's function is called at the
- appropriate time to handle the event.
+ appropriate time to handle the event. Foreign tables do not support the
+ TRUNCATE statement at all.
</para>
<para>
@@ -111,10 +112,10 @@
triggers fire immediately before a particular row is operated on,
while row-level <literal>AFTER</> triggers fire at the end of the
statement (but before any statement-level <literal>AFTER</> triggers).
- These types of triggers may only be defined on tables. Row-level
- <literal>INSTEAD OF</> triggers may only be defined on views, and fire
- immediately as each row in the view is identified as needing to be
- operated on.
+ These types of triggers may only be defined on tables and foreign tables.
+ Row-level <literal>INSTEAD OF</> triggers may only be defined on views,
+ and fire immediately as each row in the view is identified as needing to
+ be operated on.
</para>
<para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 25f01e5..7f3f730 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3180,6 +3180,9 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DisableTrig: /* DISABLE TRIGGER variants */
case AT_DisableTrigAll:
case AT_DisableTrigUser:
+ ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE);
+ pass = AT_PASS_MISC;
+ break;
case AT_EnableRule: /* ENABLE/DISABLE RULE variants */
case AT_EnableAlwaysRule:
case AT_EnableReplicaRule:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index fa74bd2..f4e4bfb 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -56,6 +56,7 @@
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/tqual.h"
+#include "utils/tuplestore.h"
/* GUC variables */
@@ -195,6 +196,30 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
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.")));
+
+ if (TRIGGER_FOR_TRUNCATE(stmt->events))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is a foreign table",
+ RelationGetRelationName(rel)),
+ errdetail("Foreign tables cannot have TRUNCATE triggers.")));
+
+ 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),
@@ -1080,10 +1105,11 @@ RemoveTriggerById(Oid trigOid)
rel = heap_open(relid, AccessExclusiveLock);
if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_VIEW)
+ rel->rd_rel->relkind != RELKIND_VIEW &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view",
+ errmsg("\"%s\" is not a table, view, or foreign table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
@@ -1184,10 +1210,12 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
form = (Form_pg_class) GETSTRUCT(tuple);
/* only tables and views can have triggers */
- if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW)
+ if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW &&
+ form->relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view", rv->relname)));
+ errmsg("\"%s\" is not a table, view, or foreign table",
+ rv->relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
@@ -2164,7 +2192,8 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
bool
ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ HeapTuple fdw_trigtuple)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
bool result = true;
@@ -2174,10 +2203,16 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
TupleTableSlot *newSlot;
int i;
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- LockTupleExclusive, &newSlot);
- if (trigtuple == NULL)
- return false;
+ Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid));
+ if (fdw_trigtuple == NULL)
+ {
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ LockTupleExclusive, &newSlot);
+ if (trigtuple == NULL)
+ return false;
+ }
+ else
+ trigtuple = fdw_trigtuple;
LocTriggerData.type = T_TriggerData;
LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
@@ -2215,29 +2250,38 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
if (newtuple != trigtuple)
heap_freetuple(newtuple);
}
- heap_freetuple(trigtuple);
+ if (trigtuple != fdw_trigtuple)
+ heap_freetuple(trigtuple);
return result;
}
void
ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
- ItemPointer tupleid)
+ ItemPointer tupleid,
+ HeapTuple fdw_trigtuple)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ HeapTuple trigtuple;
+
+ Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid));
+ if (fdw_trigtuple == NULL)
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ else
+ trigtuple = fdw_trigtuple;
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
true, trigtuple, NULL, NIL, NULL);
- heap_freetuple(trigtuple);
+ if (trigtuple != fdw_trigtuple)
+ heap_freetuple(trigtuple);
}
}
@@ -2353,7 +2397,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TupleTableSlot *
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid, TupleTableSlot *slot)
+ ItemPointer tupleid,
+ HeapTuple fdw_trigtuple,
+ TupleTableSlot *slot)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
HeapTuple slottuple = ExecMaterializeSlot(slot);
@@ -2380,11 +2426,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
else
lockmode = LockTupleNoKeyExclusive;
- /* get a copy of the on-disk tuple we are planning to update */
- trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- lockmode, &newSlot);
- if (trigtuple == NULL)
- return NULL; /* cancel the update action */
+ Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid));
+ if (fdw_trigtuple == NULL)
+ {
+ /* get a copy of the on-disk tuple we are planning to update */
+ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
+ lockmode, &newSlot);
+ if (trigtuple == NULL)
+ return NULL; /* cancel the update action */
+ }
+ else
+ {
+ trigtuple = fdw_trigtuple;
+ newSlot = NULL;
+ }
/*
* In READ COMMITTED isolation level it's possible that target tuple was
@@ -2437,11 +2492,13 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
heap_freetuple(oldtuple);
if (newtuple == NULL)
{
- heap_freetuple(trigtuple);
+ if (trigtuple != fdw_trigtuple)
+ heap_freetuple(trigtuple);
return NULL; /* "do nothing" */
}
}
- heap_freetuple(trigtuple);
+ if (trigtuple != fdw_trigtuple)
+ heap_freetuple(trigtuple);
if (newtuple != slottuple)
{
@@ -2465,23 +2522,31 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
ItemPointer tupleid, HeapTuple newtuple,
+ HeapTuple fdw_trigtuple,
List *recheckIndexes)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_row)
{
- HeapTuple trigtuple = GetTupleForTrigger(estate,
- NULL,
- relinfo,
- tupleid,
- LockTupleExclusive,
- NULL);
+ HeapTuple trigtuple;
+
+ Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid));
+ if (fdw_trigtuple == NULL)
+ trigtuple = GetTupleForTrigger(estate,
+ NULL,
+ relinfo,
+ tupleid,
+ LockTupleExclusive,
+ NULL);
+ else
+ trigtuple = fdw_trigtuple;
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
true, trigtuple, newtuple, recheckIndexes,
GetModifiedColumns(relinfo, estate));
- heap_freetuple(trigtuple);
+ if (trigtuple != fdw_trigtuple)
+ heap_freetuple(trigtuple);
}
}
@@ -2942,13 +3007,22 @@ typedef SetConstraintStateData *SetConstraintState;
* Per-trigger-event data
*
* The actual per-event data, AfterTriggerEventData, includes DONE/IN_PROGRESS
- * status bits and one or two tuple CTIDs. Each event record also has an
- * associated AfterTriggerSharedData that is shared across all instances
- * of similar events within a "chunk".
+ * status bits and up to two tuple CTIDs. Each event record also has an
+ * associated AfterTriggerSharedData that is shared across all instances of
+ * similar events within a "chunk".
*
- * We arrange not to waste storage on ate_ctid2 for non-update events.
- * We could go further and not store either ctid for statement-level triggers,
- * but that seems unlikely to be worth the trouble.
+ * 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.
+ *
+ * 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
@@ -2963,9 +3037,14 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order
* bits */
-#define AFTER_TRIGGER_2CTIDS 0x10000000
-#define AFTER_TRIGGER_DONE 0x20000000
-#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
+#define AFTER_TRIGGER_DONE 0x10000000
+#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
+/* bits describing the size and tuple sources of this event */
+#define AFTER_TRIGGER_FDW_REUSE 0x00000000
+#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_1CTID 0x40000000
+#define AFTER_TRIGGER_2CTID 0xC0000000
+#define AFTER_TRIGGER_TUP_BITS 0xC0000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -2986,16 +3065,25 @@ typedef struct AfterTriggerEventData
ItemPointerData ate_ctid2; /* new updated tuple */
} AfterTriggerEventData;
-/* This struct must exactly match the one above except for not having ctid2 */
+/* AfterTriggerEventData, minus 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_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_2CTIDS) ? \
- sizeof(AfterTriggerEventData) : sizeof(AfterTriggerEventDataOneCtid))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventData) : \
+ ((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)))
@@ -3068,7 +3156,11 @@ typedef struct AfterTriggerEventList
* immediate-mode triggers, and append any deferred events to the main events
* list.
*
- * maxquerydepth is just the allocated length of query_stack.
+ * fdw_tuplestores[query_depth] is a tuplestore containing the foreign tuples
+ * needed for the current query.
+ *
+ * maxquerydepth is just the allocated length of query_stack and
+ * fdw_tuplestores.
*
* state_stack is a stack of pointers to saved copies of the SET CONSTRAINTS
* state data; each subtransaction level that modifies that state first
@@ -3097,6 +3189,7 @@ typedef struct AfterTriggersData
AfterTriggerEventList events; /* deferred-event list */
int query_depth; /* current query list index */
AfterTriggerEventList *query_stack; /* events pending from each query */
+ Tuplestorestate **fdw_tuplestores; /* foreign tuples from each query */
int maxquerydepth; /* allocated len of above array */
MemoryContext event_cxt; /* memory context for events, if any */
@@ -3113,18 +3206,60 @@ typedef AfterTriggersData *AfterTriggers;
static AfterTriggers afterTriggers;
-
static void AfterTriggerExecute(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
- MemoryContext per_tuple_context);
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot1,
+ TupleTableSlot *trig_tuple_slot2);
static SetConstraintState SetConstraintStateCreate(int numalloc);
static SetConstraintState SetConstraintStateCopy(SetConstraintState state);
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
Oid tgoid, bool tgisdeferred);
+/*
+ * Gets the current query fdw tuplestore and initializes it if necessary
+ */
+static Tuplestorestate *
+GetCurrentFDWTuplestore()
+{
+ Tuplestorestate *ret;
+
+ ret = afterTriggers->fdw_tuplestores[afterTriggers->query_depth];
+ if (ret == NULL)
+ {
+ MemoryContext oldcxt;
+ ResourceOwner saveResourceOwner;
+
+ /*
+ * Make the tuplestore valid until end of transaction. This is the
+ * allocation lifespan of the associated events list, but we really
+ * only need it until AfterTriggerEndQuery().
+ */
+ oldcxt = MemoryContextSwitchTo(TopTransactionContext);
+ saveResourceOwner = CurrentResourceOwner;
+ PG_TRY();
+ {
+ CurrentResourceOwner = TopTransactionResourceOwner;
+ ret = tuplestore_begin_heap(false, false, work_mem);
+ }
+ PG_CATCH();
+ {
+ CurrentResourceOwner = saveResourceOwner;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ CurrentResourceOwner = saveResourceOwner;
+ MemoryContextSwitchTo(oldcxt);
+
+ afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = ret;
+ }
+
+ return ret;
+}
+
/* ----------
* afterTriggerCheckState()
*
@@ -3365,13 +3500,17 @@ afterTriggerRestoreEventList(AfterTriggerEventList *events,
* 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(AfterTriggerEvent event,
Relation rel, TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
- MemoryContext per_tuple_context)
+ MemoryContext per_tuple_context,
+ TupleTableSlot *trig_tuple_slot1,
+ TupleTableSlot *trig_tuple_slot2)
{
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
@@ -3408,34 +3547,81 @@ AfterTriggerExecute(AfterTriggerEvent event,
/*
* Fetch the required tuple(s).
*/
- if (ItemPointerIsValid(&(event->ate_ctid1)))
+ switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
- elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
- LocTriggerData.tg_trigtuple = &tuple1;
- LocTriggerData.tg_trigtuplebuf = buffer1;
- }
- else
- {
- LocTriggerData.tg_trigtuple = NULL;
- LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
- }
+ case AFTER_TRIGGER_FDW_FETCH:
+ {
+ Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
- /* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) &&
- ItemPointerIsValid(&(event->ate_ctid2)))
- {
- ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
- if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
- elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
- LocTriggerData.tg_newtuple = &tuple2;
- LocTriggerData.tg_newtuplebuf = buffer2;
- }
- else
- {
- LocTriggerData.tg_newtuple = NULL;
- LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ 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:
+ /*
+ * Using ExecMaterializeSlot() rather than ExecFetchSlotTuple()
+ * ensures 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 ExecFetchSlotTuple()
+ * must materialize anyway.
+ */
+ LocTriggerData.tg_trigtuple =
+ ExecMaterializeSlot(trig_tuple_slot1);
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+
+ if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
+ TRIGGER_EVENT_UPDATE)
+ {
+ LocTriggerData.tg_newtuple =
+ ExecMaterializeSlot(trig_tuple_slot2);
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
+ else
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+
+ break;
+
+ default:
+ if (ItemPointerIsValid(&(event->ate_ctid1)))
+ {
+ ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL))
+ elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+ LocTriggerData.tg_trigtuple = &tuple1;
+ LocTriggerData.tg_trigtuplebuf = buffer1;
+ }
+ else
+ {
+ LocTriggerData.tg_trigtuple = NULL;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ }
+
+ /* don't touch ctid2 if not there */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_2CTID &&
+ ItemPointerIsValid(&(event->ate_ctid2)))
+ {
+ ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self));
+ if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL))
+ elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+ LocTriggerData.tg_newtuple = &tuple2;
+ LocTriggerData.tg_newtuplebuf = buffer2;
+ }
+ else
+ {
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ }
}
/*
@@ -3457,7 +3643,9 @@ AfterTriggerExecute(AfterTriggerEvent event,
finfo,
NULL,
per_tuple_context);
- if (rettuple != NULL && rettuple != &tuple1 && rettuple != &tuple2)
+ if (rettuple != NULL &&
+ rettuple != LocTriggerData.tg_trigtuple &&
+ rettuple != LocTriggerData.tg_newtuple)
heap_freetuple(rettuple);
/*
@@ -3577,6 +3765,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
TriggerDesc *trigdesc = NULL;
FmgrInfo *finfo = NULL;
Instrumentation *instr = NULL;
+ TupleTableSlot *slot1 = NULL,
+ *slot2 = NULL;
/* Make a local EState if need be */
if (estate == NULL)
@@ -3621,6 +3811,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
trigdesc = rInfo->ri_TrigDesc;
finfo = rInfo->ri_TrigFunctions;
instr = rInfo->ri_TrigInstrument;
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ if (slot1 != NULL)
+ {
+ ExecDropSingleTupleTableSlot(slot1);
+ ExecDropSingleTupleTableSlot(slot2);
+ }
+ slot1 = MakeSingleTupleTableSlot(rel->rd_att);
+ slot2 = MakeSingleTupleTableSlot(rel->rd_att);
+ }
if (trigdesc == NULL) /* should not happen */
elog(ERROR, "relation %u has no triggers",
evtshared->ats_relid);
@@ -3632,7 +3832,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
* won't try to re-fire it.
*/
AfterTriggerExecute(event, rel, trigdesc, finfo, instr,
- per_tuple_context);
+ per_tuple_context, slot1, slot2);
/*
* Mark the event as done.
@@ -3663,6 +3863,11 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
events->tailfree = chunk->freeptr;
}
}
+ if (slot1 != NULL)
+ {
+ ExecDropSingleTupleTableSlot(slot1);
+ ExecDropSingleTupleTableSlot(slot2);
+ }
/* Release working resources */
MemoryContextDelete(per_tuple_context);
@@ -3712,10 +3917,13 @@ AfterTriggerBeginXact(void)
afterTriggers->events.tailfree = NULL;
afterTriggers->query_depth = -1;
- /* We initialize the query stack to a reasonable size */
+ /* We initialize the arrays to a reasonable size */
afterTriggers->query_stack = (AfterTriggerEventList *)
MemoryContextAlloc(TopTransactionContext,
8 * sizeof(AfterTriggerEventList));
+ afterTriggers->fdw_tuplestores = (Tuplestorestate **)
+ MemoryContextAllocZero(TopTransactionContext,
+ 8 * sizeof(Tuplestorestate *));
afterTriggers->maxquerydepth = 8;
/* Context for events is created only when needed */
@@ -3756,11 +3964,18 @@ AfterTriggerBeginQuery(void)
if (afterTriggers->query_depth >= afterTriggers->maxquerydepth)
{
/* repalloc will keep the stack in the same context */
- int new_alloc = afterTriggers->maxquerydepth * 2;
+ int old_alloc = afterTriggers->maxquerydepth;
+ int new_alloc = old_alloc * 2;
afterTriggers->query_stack = (AfterTriggerEventList *)
repalloc(afterTriggers->query_stack,
new_alloc * sizeof(AfterTriggerEventList));
+ afterTriggers->fdw_tuplestores = (Tuplestorestate **)
+ repalloc(afterTriggers->fdw_tuplestores,
+ new_alloc * sizeof(Tuplestorestate *));
+ /* Clear newly-allocated slots for subsequent lazy initialization. */
+ memset(afterTriggers->fdw_tuplestores + old_alloc,
+ 0, (new_alloc - old_alloc) * sizeof(Tuplestorestate *));
afterTriggers->maxquerydepth = new_alloc;
}
@@ -3788,6 +4003,7 @@ void
AfterTriggerEndQuery(EState *estate)
{
AfterTriggerEventList *events;
+ Tuplestorestate *fdw_tuplestore;
/* Must be inside a transaction */
Assert(afterTriggers != NULL);
@@ -3828,7 +4044,13 @@ AfterTriggerEndQuery(EState *estate)
break;
}
- /* Release query-local storage for events */
+ /* Release query-local storage for events, including tuplestore if any */
+ fdw_tuplestore = afterTriggers->fdw_tuplestores[afterTriggers->query_depth];
+ if (fdw_tuplestore)
+ {
+ tuplestore_end(fdw_tuplestore);
+ afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = NULL;
+ }
afterTriggerFreeEventList(&afterTriggers->query_stack[afterTriggers->query_depth]);
afterTriggers->query_depth--;
@@ -4050,6 +4272,15 @@ AfterTriggerEndSubXact(bool isCommit)
*/
while (afterTriggers->query_depth > afterTriggers->depth_stack[my_level])
{
+ Tuplestorestate *ts;
+
+ ts = afterTriggers->fdw_tuplestores[afterTriggers->query_depth];
+ if (ts)
+ {
+ tuplestore_end(ts);
+ afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = NULL;
+ }
+
afterTriggerFreeEventList(&afterTriggers->query_stack[afterTriggers->query_depth]);
afterTriggers->query_depth--;
}
@@ -4546,9 +4777,11 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
+ char relkind = relinfo->ri_RelationDesc->rd_rel->relkind;
int tgtype_event;
int tgtype_level;
int i;
+ Tuplestorestate *fdw_tuplestore = NULL;
/*
* Check state. We use normal tests not Asserts because it is possible to
@@ -4567,7 +4800,6 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* validation is important to make sure we don't walk off the edge of our
* arrays.
*/
- new_event.ate_flags = 0;
switch (event)
{
case TRIGGER_EVENT_INSERT:
@@ -4612,7 +4844,6 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newtup != NULL);
ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
- new_event.ate_flags |= AFTER_TRIGGER_2CTIDS;
}
else
{
@@ -4635,6 +4866,11 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
+ if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
+ AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ /* else, we'll initialize ate_flags for each trigger */
+
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
for (i = 0; i < trigdesc->numtriggers; i++)
@@ -4650,6 +4886,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldtup, newtup))
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
@@ -4711,6 +4959,15 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
afterTriggerAddEvent(&afterTriggers->query_stack[afterTriggers->query_depth],
&new_event, &new_shared);
}
+
+ /* Finally, add the tuple(s) to the tuplestore if needed. */
+ if (fdw_tuplestore)
+ {
+ if (oldtup != NULL)
+ tuplestore_puttuple(fdw_tuplestore, oldtup);
+ if (newtup != NULL)
+ tuplestore_puttuple(fdw_tuplestore, newtup);
+ }
}
Datum
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 6f0f47e..05f1248 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -309,15 +309,17 @@ ExecInsert(TupleTableSlot *slot,
* delete and oldtuple is NULL. When deleting from a view,
* oldtuple is passed to the INSTEAD OF triggers and identifies
* what to delete, and tupleid is invalid. When deleting from a
- * foreign table, both tupleid and oldtuple are NULL; the FDW has
- * to figure out which row to delete using data from the planSlot.
+ * foreign table, tupleid is invalid; the FDW has to figure out
+ * which row to delete using data from the planSlot. oldtuple is
+ * passed to foreign table triggers; it is NULL when the foreign
+ * table has no relevant triggers.
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
- HeapTupleHeader oldtuple,
+ HeapTuple oldtuple,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate,
@@ -342,7 +344,7 @@ ExecDelete(ItemPointer tupleid,
bool dodelete;
dodelete = ExecBRDeleteTriggers(estate, epqstate, resultRelInfo,
- tupleid);
+ tupleid, oldtuple);
if (!dodelete) /* "do nothing" */
return NULL;
@@ -352,16 +354,10 @@ ExecDelete(ItemPointer tupleid,
if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_delete_instead_row)
{
- HeapTupleData tuple;
bool dodelete;
Assert(oldtuple != NULL);
- tuple.t_data = oldtuple;
- tuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
- ItemPointerSetInvalid(&(tuple.t_self));
- tuple.t_tableOid = InvalidOid;
-
- dodelete = ExecIRDeleteTriggers(estate, resultRelInfo, &tuple);
+ dodelete = ExecIRDeleteTriggers(estate, resultRelInfo, oldtuple);
if (!dodelete) /* "do nothing" */
return NULL;
@@ -488,7 +484,7 @@ ldelete:;
(estate->es_processed)++;
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
@@ -512,10 +508,7 @@ ldelete:;
slot = estate->es_trig_tuple_slot;
if (oldtuple != NULL)
{
- deltuple.t_data = oldtuple;
- deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
- ItemPointerSetInvalid(&(deltuple.t_self));
- deltuple.t_tableOid = InvalidOid;
+ deltuple = *oldtuple;
delbuffer = InvalidBuffer;
}
else
@@ -564,15 +557,17 @@ ldelete:;
* update and oldtuple is NULL. When updating a view, oldtuple
* is passed to the INSTEAD OF triggers and identifies what to
* update, and tupleid is invalid. When updating a foreign table,
- * both tupleid and oldtuple are NULL; the FDW has to figure out
- * which row to update using data from the planSlot.
+ * tupleid is invalid; the FDW has to figure out which row to
+ * update using data from the planSlot. oldtuple is passed to
+ * foreign table triggers; it is NULL when the foreign table has
+ * no relevant triggers.
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecUpdate(ItemPointer tupleid,
- HeapTupleHeader oldtuple,
+ HeapTuple oldtuple,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EPQState *epqstate,
@@ -609,7 +604,7 @@ ExecUpdate(ItemPointer tupleid,
resultRelInfo->ri_TrigDesc->trig_update_before_row)
{
slot = ExecBRUpdateTriggers(estate, epqstate, resultRelInfo,
- tupleid, slot);
+ tupleid, oldtuple, slot);
if (slot == NULL) /* "do nothing" */
return NULL;
@@ -622,16 +617,8 @@ ExecUpdate(ItemPointer tupleid,
if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_update_instead_row)
{
- HeapTupleData oldtup;
-
- Assert(oldtuple != NULL);
- oldtup.t_data = oldtuple;
- oldtup.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
- ItemPointerSetInvalid(&(oldtup.t_self));
- oldtup.t_tableOid = InvalidOid;
-
slot = ExecIRUpdateTriggers(estate, resultRelInfo,
- &oldtup, slot);
+ oldtuple, slot);
if (slot == NULL) /* "do nothing" */
return NULL;
@@ -788,7 +775,7 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
+ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, oldtuple,
recheckIndexes);
list_free(recheckIndexes);
@@ -873,7 +860,8 @@ ExecModifyTable(ModifyTableState *node)
TupleTableSlot *planSlot;
ItemPointer tupleid = NULL;
ItemPointerData tuple_ctid;
- HeapTupleHeader oldtuple = NULL;
+ HeapTupleData oldtupdata;
+ HeapTuple oldtuple;
/*
* This should NOT get called during EvalPlanQual; we should have passed a
@@ -958,6 +946,7 @@ ExecModifyTable(ModifyTableState *node)
EvalPlanQualSetSlot(&node->mt_epqstate, planSlot);
slot = planSlot;
+ oldtuple = NULL;
if (junkfilter != NULL)
{
/*
@@ -984,11 +973,15 @@ ExecModifyTable(ModifyTableState *node)
* ctid!! */
tupleid = &tuple_ctid;
}
- else if (relkind == RELKIND_FOREIGN_TABLE)
- {
- /* do nothing; FDW must fetch any junk attrs it wants */
- }
- else
+ /*
+ * Foreign table updates have a wholerow attribute when the
+ * relation has an AFTER ROW trigger. Quite separately, the
+ * FDW may fetch its own junk attrs to identify the row.
+ *
+ * Other relevant relkinds, currently limited to views, always
+ * have a wholerow attribute.
+ */
+ else if (AttributeNumberIsValid(junkfilter->jf_junkAttNo))
{
datum = ExecGetJunkAttribute(slot,
junkfilter->jf_junkAttNo,
@@ -997,8 +990,20 @@ ExecModifyTable(ModifyTableState *node)
if (isNull)
elog(ERROR, "wholerow is NULL");
- oldtuple = DatumGetHeapTupleHeader(datum);
+ oldtupdata.t_data = DatumGetHeapTupleHeader(datum);
+ oldtupdata.t_len =
+ HeapTupleHeaderGetDatumLength(oldtupdata.t_data);
+ /* XXX Wrong for foreign tables, but we lack the data. */
+ ItemPointerSetInvalid(&(oldtupdata.t_self));
+ /* Historically, view triggers see invalid t_tableOid. */
+ oldtupdata.t_tableOid =
+ (relkind == RELKIND_VIEW) ? InvalidOid :
+ RelationGetRelid(resultRelInfo->ri_RelationDesc);
+
+ oldtuple = &oldtupdata;
}
+ else
+ Assert(relkind == RELKIND_FOREIGN_TABLE);
}
/*
@@ -1334,7 +1339,11 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
- /* FDW must fetch any junk attrs it wants */
+ /*
+ * When there is an AFTER trigger, there should be a
+ * wholerow attribute.
+ */
+ j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow");
}
else
{
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 35bda67..52cc5b7 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -240,7 +240,22 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->commandType = parse->commandType;
result->queryId = parse->queryId;
- result->hasReturning = (parse->returningList != NIL);
+
+ /*
+ * Mark the result as having RETURNING only if the returning target list
+ * has non-resjunk entries
+ */
+ result->hasReturning = false;
+ foreach(lp, parse->returningList)
+ {
+ TargetEntry *tle = lfirst(lp);
+
+ if (!tle->resjunk)
+ {
+ result->hasReturning = true;
+ break;
+ }
+ }
result->hasModifyingCTE = parse->hasModifyingCTE;
result->canSetTag = parse->canSetTag;
result->transientPlan = glob->transientPlan;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 3728d8c..06a2f33 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -51,7 +51,8 @@ static Query *rewriteRuleAction(Query *parsetree,
CmdType event,
bool *returning_flag);
static List *adjustJoinTreeList(Query *parsetree, bool removert, int rt_index);
-static void rewriteTargetListIU(Query *parsetree, Relation target_relation,
+static void rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte,
+ Relation target_relation,
List **attrno_list);
static TargetEntry *process_matched_tle(TargetEntry *src_tle,
TargetEntry *prior_tle,
@@ -667,6 +668,11 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
* 4. Sort the tlist into standard order: non-junk fields in order by resno,
* then junk fields (these in no particular order).
*
+ * 5. For an INSERT or UPDATE on a foreign table with an AFTER ROW trigger,
+ * add a whole-row attribute to the RETURNING list. This signals to the FDW
+ * that it must fetch all attributes from the remote side. XXX Won't this
+ * confuse code like FetchStatementTargetList()?
+ *
* We must do items 1,2,3 before firing rewrite rules, else rewritten
* references to NEW.foo will produce wrong or incomplete results. Item 4
* is not needed for rewriting, but will be needed by the planner, and we
@@ -678,8 +684,8 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
* processing VALUES RTEs.
*/
static void
-rewriteTargetListIU(Query *parsetree, Relation target_relation,
- List **attrno_list)
+rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte,
+ Relation target_relation, List **attrno_list)
{
CmdType commandType = parsetree->commandType;
TargetEntry **new_tles;
@@ -755,6 +761,30 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation,
}
}
+ /*
+ * For foreign tables, force RETURNING the whole-row if a corresponding
+ * AFTER trigger is found
+ */
+ if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE &&
+ target_relation->trigdesc &&
+ ((commandType == CMD_INSERT &&
+ target_relation->trigdesc->trig_insert_after_row) ||
+ (commandType == CMD_UPDATE &&
+ target_relation->trigdesc->trig_update_after_row)))
+
+ {
+ Var *var = makeWholeRowVar(target_rte,
+ parsetree->resultRelation,
+ 0,
+ false);
+ TargetEntry *tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->returningList) + 1,
+ "wholerow",
+ true);
+
+ parsetree->returningList = lappend(parsetree->returningList, tle);
+ }
+
for (attrno = 1; attrno <= numattrs; attrno++)
{
TargetEntry *new_tle = new_tles[attrno - 1];
@@ -1199,7 +1229,7 @@ static void
rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
Relation target_relation)
{
- Var *var;
+ Var *var = NULL;
const char *attrname;
TargetEntry *tle;
@@ -1231,7 +1261,26 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
target_relation);
- return;
+ /*
+ * If we have a trigger corresponding to the operation, add a wholerow
+ * attribute. XXX This misses system columns.
+ */
+ if (target_relation->trigdesc &&
+ ((parsetree->commandType == CMD_UPDATE &&
+ (target_relation->trigdesc->trig_update_after_row
+ || target_relation->trigdesc->trig_update_before_row)) ||
+ (parsetree->commandType == CMD_DELETE &&
+ (target_relation->trigdesc->trig_delete_after_row ||
+ target_relation->trigdesc->trig_delete_before_row))))
+ {
+ var = makeWholeRowVar(target_rte,
+ parsetree->resultRelation,
+ 0,
+ false);
+
+ attrname = "wholerow";
+
+ }
}
else
{
@@ -1247,12 +1296,15 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
attrname = "wholerow";
}
- tle = makeTargetEntry((Expr *) var,
- list_length(parsetree->targetList) + 1,
- pstrdup(attrname),
- true);
+ if (var != NULL)
+ {
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->targetList) + 1,
+ pstrdup(attrname),
+ true);
- parsetree->targetList = lappend(parsetree->targetList, tle);
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+ }
}
@@ -2993,19 +3045,19 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
List *attrnos;
/* Process the main targetlist ... */
- rewriteTargetListIU(parsetree, rt_entry_relation, &attrnos);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, &attrnos);
/* ... and the VALUES expression lists */
rewriteValuesRTE(values_rte, rt_entry_relation, attrnos);
}
else
{
/* Process just the main targetlist */
- rewriteTargetListIU(parsetree, rt_entry_relation, NULL);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL);
}
}
else if (event == CMD_UPDATE)
{
- rewriteTargetListIU(parsetree, rt_entry_relation, NULL);
+ rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL);
rewriteTargetListUD(parsetree, rt_entry, rt_entry_relation);
}
else if (event == CMD_DELETE)
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 18cb128..84bc984 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -147,10 +147,12 @@ extern void ExecASDeleteTriggers(EState *estate,
extern bool ExecBRDeleteTriggers(EState *estate,
EPQState *epqstate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ HeapTuple fdw_trigtuple);
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
- ItemPointer tupleid);
+ ItemPointer tupleid,
+ HeapTuple fdw_trigtuple);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -162,11 +164,13 @@ extern TupleTableSlot *ExecBRUpdateTriggers(EState *estate,
EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
+ HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple newtuple,
+ HeapTuple fdw_trigtuple,
List *recheckIndexes);
extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 60506e0..c34c9b4 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -1158,6 +1158,43 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
ERROR: must be owner of foreign server s9
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+ERROR: "foreign_table_1" is a foreign table
+DETAIL: Foreign tables cannot have constraint triggers.
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1
+ DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1
+ ENABLE TRIGGER trigtest_before_stmt;
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+DROP FUNCTION dummy_trigger();
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
ERROR: foreign table "no_table" does not exist
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index f819eb1..0f0869e 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -470,6 +470,50 @@ CREATE USER MAPPING FOR current_user SERVER s9;
DROP SERVER s9 CASCADE; -- ERROR
RESET ROLE;
+-- Triggers
+CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE
+ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger();
+
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1
+ DISABLE TRIGGER trigtest_before_stmt;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1
+ ENABLE TRIGGER trigtest_before_stmt;
+
+DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
+DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+
+DROP FUNCTION dummy_trigger();
+
-- DROP FOREIGN TABLE
DROP FOREIGN TABLE no_table; -- ERROR
DROP FOREIGN TABLE IF EXISTS no_table;
I hacked on this for awhile, but there remain two matters on which I'm
uncertain about the right way forward.(1) To acquire the old tuple for UPDATE/DELETE operations, the patch closely
parallels our handling for INSTEAD OF triggers on views. It adds a wholerow
resjunk attribute, from which it constructs a HeapTuple before calling a
trigger function. This loses the system columns, an irrelevant
consideration for views but meaningful for foreign tables. postgres_fdw
maintains the "ctid" system column (t_self), but triggers will always see
an invalid t_self for the old tuple. One way to fix this is to pass around
the old tuple data as ROW(ctid, oid, xmin, cmin, xmax, cmax, tableoid,
wholerow). That's fairly close to sufficient, but it doesn't cover t_ctid.
Frankly, I would like to find a principled excuse to not worry about making
foreign table system columns accessible from triggers. Supporting system
columns dramatically affects the mechanism, and what trigger is likely to
care? Unfortunately, that argument seems too weak. Does anyone have a
cleaner idea for keeping track of the system column values or a stronger
argument for not bothering?
Regarding to the first suggestion,
I think, it is better not to care about system columns on foreign tables,
because it fully depends on driver's implementation whether FDW fetches
"ctid" from its data source, or not.
Usually, system columns of foreign table, except for tableoid, are nonsense.
Because of implementation reason, postgres_fdw fetches "ctid" of remote
tables on UPDATE / DELETE, it is not a common nature for all FDW drivers.
For example, we can assume an implementation that uses primary key of remote
table to identify the record to be updated or deleted. In this case, local
"ctid" does not have meaningful value.
So, fundamentally, we cannot guarantee FDW driver returns meaningful "ctid"
or other system columns.
(2) When a foreign table has an AFTER ROW trigger, the FDW's
ExecForeign{Insert,Update,Delete} callbacks must return a slot covering
all columns. Current FDW API documentation says things like this:The data in the returned slot is used only if the INSERT query has a
RETURNING clause. Hence, the FDW could choose to optimize away returning
some or all columns depending on the contents of the RETURNING clause.Consistent with that, postgres_fdw inspects the returningLists of the
ModifyTable node to decide which columns are necessary. This patch has
rewriteTargetListIU() add a resjunk wholerow var to the returningList of
any query that will fire an AFTER ROW trigger on a foreign table. That
avoids the need to change the FDW API contract or any postgres_fdw code.
I was pleased about that for a time, but on further review, I'm doubting
that the benefit for writable FDWs justifies muddying the definition of
returningList; until now, returningList has been free from resjunk TLEs.
For example, callers of
FetchStatementTargetList() may be unprepared to see an all-resjunk list,
instead of NIL, for a data modification statement that returns nothing.If we do keep the patch's approach, I'm inclined to rename returningList.
However, I more lean toward adding a separate flag to indicate the need
to return a complete tuple regardless of the RETURNING list. The benefits
of overloading returningList are all short-term benefits. We know that
the FDW API is still converging, so changing it seems eventually-preferable
to, and safer than, changing the name or meaning of returningList.
Thoughts?On Thu, Mar 06, 2014 at 09:11:19AM +0100, Ronan Dunklau wrote:
Le mercredi 5 mars 2014 22:36:44 Noah Misch a écrit :
Agreed. More specifically, I see only two scenarios for retrieving
tuples from the tuplestore. Scenario one is that we need the next
tuple (or pair of tuples, depending on the TriggerEvent). Scenario
two is that we need the tuple(s) most recently retrieved. If that's
correct, I'm inclined to rearrange afterTriggerInvokeEvents() and
AfterTriggerExecute() to remember the tuple or pair of tuples most
recently retrieved. They'll then never call tuplestore_advance()
just to reposition. Do you see a problem with that?I don't see any problem with that. I don't know how this would be
implemented, but it would make sense to avoid those scans, as long as
a fresh copy is passed to the trigger: modifications to a tuple
performed in an after trigger should not be visible to the next one.Trigger functions are not permitted to modify tg_trigtuple or tg_newtuple;
notice that, for non-foreign triggers, we pass shared_buffers-backed tuples
in those fields. Therefore, no copy is necessary.I was again somewhat tempted to remove ate_tupleindex, perhaps by
defining the four flag bits this way:#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* two bits describing the size of and tuple sources for this event*/
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
#define AFTER_TRIGGER_FDW_FETCH 0x40000000
#define AFTER_TRIGGER_1CTID 0x80000000
#define AFTER_TRIGGER_2CTID 0xC0000000AFTER_TRIGGER_FDW_FETCH and AFTER_TRIGGER_FDW_REUSE correspond to
the aforementioned scenarios one and two, respectively. I think,
though, I'll rate this as needless micro-optimization and not bother;opinions welcome.
(The savings is four bytes per foreign table trigger event.)
I was already happy with having a lower footprint for foreign table
trigger events than for regular trigger events, but if we remove the
need for seeking in the tuplestore entirely, it would make sense to getrid of the index.
I'm pleased with how this turned out. Besides the memory savings in
question, this removed the INT_MAX limitation and simplified the code
overall. I did not observe a notable runtime improvement, though that's
unsurprising.Other notable changes in the attached revision:
1. UPDATE/DELETE row-level triggers on foreign tables and INSTEAD OF
triggers on views have a similar requirement to generate a HeapTuple
representing the old row. View triggers did so in nodeModifyTable.c, while
foreign table triggers did so in trigger.c. Both were valid choices, but
the code siting should not be relkind-dependent without good reason. I
centralized this in ExecModifyTable().2. Made CREATE TRIGGER forbid INSTEAD OF and TRUNCATE triggers on foreign
tables. The documentation already claimed they were unavailable.3. Fixed pointer arithmetic in AfterTriggerBeginQuery()'s MemSet() call.
4. Modified GetCurrentFDWTuplestore() to allocate the tuplestore in
TopTransactionContext. We explicitly put the events list there
(specifically, in a documentation-only child of that context), so it seemed
more consistent to do the same for the associated foreign tuples. I did
not find any live bug from the previous coding, because CurrentMemoryContext
always happened to be one that survived past AfterTriggerEndQuery().5. Updated comments and documentation that still reflected earlier versions
of the patch, as well as older comments obsoleted by the patch.6. Reverted cosmetic changes, like addition of braces and blank lines, to
passages of code not otherwise changing. Please see:
https://wiki.postgresql.org/wiki/Creating_Clean_Patches--
Noah Misch
EnterpriseDB
http://www.enterprisedb.com
--
NEC OSS Promotion Center / PG-Strom Project
KaiGai Kohei <kaigai@ak.jp.nec.com>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Le mardi 18 mars 2014 03:54:19 Kouhei Kaigai a écrit :
I hacked on this for awhile, but there remain two matters on which I'm
uncertain about the right way forward.(1) To acquire the old tuple for UPDATE/DELETE operations, the patch
closely
parallels our handling for INSTEAD OF triggers on views. It
adds a wholerow resjunk attribute, from which it constructs a HeapTuple
before calling a trigger function. This loses the system columns, an
irrelevant
consideration for views but meaningful for foreign tables. postgres_fdw
maintains the "ctid" system column (t_self), but triggers will always see
an invalid t_self for the old tuple. One way to fix this is to pass
around
the old tuple data as ROW(ctid, oid, xmin, cmin, xmax, cmax,
tableoid, wholerow). That's fairly close to sufficient, but it doesn't
cover t_ctid. Frankly, I would like to find a principled excuse to not
worry about making foreign table system columns accessible from triggers.
Supporting system columns dramatically affects the mechanism, and what
trigger is likely to care? Unfortunately, that argument seems too weak.
Does anyone have a cleaner idea for keeping track of the system column
values or a stronger argument for not bothering?Regarding to the first suggestion,
I think, it is better not to care about system columns on foreign tables,
because it fully depends on driver's implementation whether FDW fetches
"ctid" from its data source, or not.
Usually, system columns of foreign table, except for tableoid, are
nonsense.
Because of implementation reason, postgres_fdw fetches "ctid" of
remote tables on UPDATE / DELETE, it is not a common nature for all FDW
drivers. For example, we can assume an implementation that uses primary key
of remote table to identify the record to be updated or deleted. In this
case, local "ctid" does not have meaningful value.
So, fundamentally, we cannot guarantee FDW driver returns meaningful "ctid"
or other system columns.
I agree, I think it is somewhat clunky to have postgres_fdw use a feature that
is basically meaningless for other FDWs. Looking at some threads in this list,
it confused many people.
This is off-topic, but maybe we could devise an API allowing for local "system
attributes" on foreign table. This would allow FDWs to carry attributes that
weren't declared as part of the table definition. This could then be used for
postgres_fdw ctid, as well as others foreign data wrappers equivalent of an
implicit "tuple id".
(2) When a foreign table has an AFTER ROW trigger, the FDW's
ExecForeign{Insert,Update,Delete} callbacks must return a slot covering
all columns. Current FDW API documentation says things like this:The data in the returned slot is used only if the INSERT query has a
RETURNING clause. Hence, the FDW could choose to optimize away
returning
some or all columns depending on the contents of the RETURNING clause.Consistent with that, postgres_fdw inspects the returningLists of the
ModifyTable node to decide which columns are necessary. This patch has
rewriteTargetListIU() add a resjunk wholerow var to the returningList of
any query that will fire an AFTER ROW trigger on a foreign table. That
avoids the need to change the FDW API contract or any postgres_fdw code.
I was pleased about that for a time, but on further review, I'm doubting
that the benefit for writable FDWs justifies muddying the definition of
returningList; until now, returningList has been free from resjunk TLEs.
For example, callers of
FetchStatementTargetList() may be unprepared to see an all-resjunk list,
instead of NIL, for a data modification statement that returns nothing.If we do keep the patch's approach, I'm inclined to rename returningList.
However, I more lean toward adding a separate flag to indicate the need
to return a complete tuple regardless of the RETURNING list. The
benefits
of overloading returningList are all short-term benefits. We know that
the FDW API is still converging, so changing it seems
eventually-preferable
to, and safer than, changing the name or meaning
of returningList. Thoughts?
On Thu, Mar 06, 2014 at 09:11:19AM +0100, Ronan Dunklau wrote:
Le mercredi 5 mars 2014 22:36:44 Noah Misch a écrit :
Agreed. More specifically, I see only two scenarios for retrieving
tuples from the tuplestore. Scenario one is that we need the next
tuple (or pair of tuples, depending on the TriggerEvent). Scenario
two is that we need the tuple(s) most recently retrieved. If that's
correct, I'm inclined to rearrange afterTriggerInvokeEvents() and
AfterTriggerExecute() to remember the tuple or pair of tuples most
recently retrieved. They'll then never call tuplestore_advance()
just to reposition. Do you see a problem with that?I don't see any problem with that. I don't know how this would be
implemented, but it would make sense to avoid those scans, as long as
a fresh copy is passed to the trigger: modifications to a tuple
performed in an after trigger should not be visible to the next one.Trigger functions are not permitted to modify tg_trigtuple or
tg_newtuple;
notice that, for non-foreign triggers, we pass shared_buffers-backed
tuples
in those fields. Therefore, no copy is necessary.
I was again somewhat tempted to remove ate_tupleindex, perhaps by
defining the four flag bits this way:#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* two bits describing the size of and tuple sources for this event*/
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
#define AFTER_TRIGGER_FDW_FETCH 0x40000000
#define AFTER_TRIGGER_1CTID 0x80000000
#define AFTER_TRIGGER_2CTID 0xC0000000AFTER_TRIGGER_FDW_FETCH and AFTER_TRIGGER_FDW_REUSE correspond to
the aforementioned scenarios one and two, respectively. I think,
though, I'll rate this as needless micro-optimization and not bother;opinions welcome.
(The savings is four bytes per foreign table trigger event.)
I was already happy with having a lower footprint for foreign table
trigger events than for regular trigger events, but if we remove the
need for seeking in the tuplestore entirely, it would make sense to getrid of the index.
I'm pleased with how this turned out. Besides the memory savings in
question, this removed the INT_MAX limitation and simplified the code
overall. I did not observe a notable runtime improvement, though that's
unsurprising.Other notable changes in the attached revision:
1. UPDATE/DELETE row-level triggers on foreign tables and INSTEAD OF
triggers on views have a similar requirement to generate a HeapTuple
representing the old row. View triggers did so in nodeModifyTable.c,
while
foreign table triggers did so in trigger.c. Both were valid
choices, but the code siting should not be relkind-dependent without good
reason. I centralized this in ExecModifyTable().2. Made CREATE TRIGGER forbid INSTEAD OF and TRUNCATE triggers on foreign
tables. The documentation already claimed they were unavailable.3. Fixed pointer arithmetic in AfterTriggerBeginQuery()'s MemSet() call.
4. Modified GetCurrentFDWTuplestore() to allocate the tuplestore in
TopTransactionContext. We explicitly put the events list there
(specifically, in a documentation-only child of that context), so it
seemed
more consistent to do the same for the associated foreign tuples.
I did not find any live bug from the previous coding, because
CurrentMemoryContext always happened to be one that survived past
AfterTriggerEndQuery().
5. Updated comments and documentation that still reflected earlier
versions
of the patch, as well as older comments obsoleted by the patch.
6. Reverted cosmetic changes, like addition of braces and blank lines, to
passages of code not otherwise changing. Please see:
https://wiki.postgresql.org/wiki/Creating_Clean_Patches--
Noah Misch
EnterpriseDB
http://www.enterprisedb.com--
NEC OSS Promotion Center / PG-Strom Project
KaiGai Kohei <kaigai@ak.jp.nec.com>
--
Ronan Dunklau
http://dalibo.com - http://dalibo.org
On Mon, Mar 17, 2014 at 11:54 PM, Kouhei Kaigai <kaigai@ak.jp.nec.com> wrote:
I hacked on this for awhile, but there remain two matters on which I'm
uncertain about the right way forward.(1) To acquire the old tuple for UPDATE/DELETE operations, the patch closely
parallels our handling for INSTEAD OF triggers on views. It adds a wholerow
resjunk attribute, from which it constructs a HeapTuple before calling a
trigger function. This loses the system columns, an irrelevant
consideration for views but meaningful for foreign tables. postgres_fdw
maintains the "ctid" system column (t_self), but triggers will always see
an invalid t_self for the old tuple. One way to fix this is to pass around
the old tuple data as ROW(ctid, oid, xmin, cmin, xmax, cmax, tableoid,
wholerow). That's fairly close to sufficient, but it doesn't cover t_ctid.
Frankly, I would like to find a principled excuse to not worry about making
foreign table system columns accessible from triggers. Supporting system
columns dramatically affects the mechanism, and what trigger is likely to
care? Unfortunately, that argument seems too weak. Does anyone have a
cleaner idea for keeping track of the system column values or a stronger
argument for not bothering?Regarding to the first suggestion,
I think, it is better not to care about system columns on foreign tables,
because it fully depends on driver's implementation whether FDW fetches
"ctid" from its data source, or not.
Usually, system columns of foreign table, except for tableoid, are nonsense.
Because of implementation reason, postgres_fdw fetches "ctid" of remote
tables on UPDATE / DELETE, it is not a common nature for all FDW drivers.
For example, we can assume an implementation that uses primary key of remote
table to identify the record to be updated or deleted. In this case, local
"ctid" does not have meaningful value.
So, fundamentally, we cannot guarantee FDW driver returns meaningful "ctid"
or other system columns.
I'm not sure I particularly agree with this reasoning - after all,
just because some people might not find a feature useful isn't a
reason not to have it. On the other hand, I don't think it's a very
useful feature, and I don't feel like we have to have it. Most system
columns can't be updated or indexed, and none of them can be dropped
or renamed, so it's not like they aren't second-class citizens to some
degree already.
By way of comparison, the first version of the index-only scan patch
gave up when it saw an expression index, instead of making an effort
to figure out whether a matching expression was present in the query.
Somebody could have looked at that patch and said, oh, well, that's an
ugly and unacceptable limitation, and we ought to reject the patch
until it's fixed. Well, instead, Tom committed the patch, and we
still have that limitation today, and it's still something we really
ought to fix some day, but in the meantime we have the feature.
Obviously, the line between "your patch is only part of a feature,
please finish it and try again" and "your patch implements a nice
self-contained feature and there are some more things that we could
build on top of it later" is to some extent a matter of judgement; but
for what it's worth, I can't get too excited about this particular
limitation of this particular patch. I just don't think that very
many people are going to miss the functionality in question.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Mar 18, 2014 at 09:31:06AM +0100, Ronan Dunklau wrote:
Le mardi 18 mars 2014 03:54:19 Kouhei Kaigai a �crit :
(1) To acquire the old tuple for UPDATE/DELETE operations, the patch
closelyparallels our handling for INSTEAD OF triggers on views. It
adds a wholerow resjunk attribute, from which it constructs a HeapTuple
before calling a trigger function. This loses the system columns, an
irrelevant
consideration for views but meaningful for foreign tables. postgres_fdw
maintains the "ctid" system column (t_self), but triggers will always see
an invalid t_self for the old tuple. One way to fix this is to pass
aroundthe old tuple data as ROW(ctid, oid, xmin, cmin, xmax, cmax,
tableoid, wholerow). That's fairly close to sufficient, but it doesn't
cover t_ctid. Frankly, I would like to find a principled excuse to not
worry about making foreign table system columns accessible from triggers.
Supporting system columns dramatically affects the mechanism, and what
trigger is likely to care? Unfortunately, that argument seems too weak.
Does anyone have a cleaner idea for keeping track of the system column
values or a stronger argument for not bothering?Regarding to the first suggestion,
I think, it is better not to care about system columns on foreign tables,
because it fully depends on driver's implementation whether FDW fetches
"ctid" from its data source, or not.
Usually, system columns of foreign table, except for tableoid, are
nonsense.Because of implementation reason, postgres_fdw fetches "ctid" of
remote tables on UPDATE / DELETE, it is not a common nature for all FDW
drivers. For example, we can assume an implementation that uses primary key
of remote table to identify the record to be updated or deleted. In this
case, local "ctid" does not have meaningful value.
So, fundamentally, we cannot guarantee FDW driver returns meaningful "ctid"
or other system columns.I agree, I think it is somewhat clunky to have postgres_fdw use a feature that
is basically meaningless for other FDWs. Looking at some threads in this list,
it confused many people.
My own reasoning for accepting omission of system columns is more along the
lines of Robert's argument. Regardless, three folks voting to do so and none
against suffices for me. I documented the system columns limitation, made the
returningList change I mentioned, and committed the patch.
This is off-topic, but maybe we could devise an API allowing for local "system
attributes" on foreign table. This would allow FDWs to carry attributes that
weren't declared as part of the table definition. This could then be used for
postgres_fdw ctid, as well as others foreign data wrappers equivalent of an
implicit "tuple id".
We could, but I discourage it. System columns are a legacy feature; I doubt
we'd choose that design afresh today. On the off-chance that you need the
value of a remote system column, you can already declare an ordinary foreign
table column for it. I raised the issue because it's inconsistent for
RETURNING to convey system columns while tg_trigtuple/tg_newtuple do not, not
because acquiring system columns from foreign tables is notably useful.
Thanks,
nm
--
Noah Misch
EnterpriseDB http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Le dimanche 23 mars 2014 02:44:26 Noah Misch a écrit :
On Tue, Mar 18, 2014 at 09:31:06AM +0100, Ronan Dunklau wrote:
Le mardi 18 mars 2014 03:54:19 Kouhei Kaigai a écrit :
(1) To acquire the old tuple for UPDATE/DELETE operations, the patch
closelyparallels our handling for INSTEAD OF triggers on views. It
adds a wholerow resjunk attribute, from which it constructs a
HeapTuple
before calling a trigger function. This loses the system columns, an
irrelevant
consideration for views but meaningful for foreign tables.
postgres_fdw
maintains the "ctid" system column (t_self), but triggers will always
see
an invalid t_self for the old tuple. One way to fix this is to pass
aroundthe old tuple data as ROW(ctid, oid, xmin, cmin, xmax, cmax,
tableoid, wholerow). That's fairly close to sufficient, but it
doesn't
cover t_ctid. Frankly, I would like to find a principled excuse to not
worry about making foreign table system columns accessible from
triggers.Supporting system columns dramatically affects the mechanism, and
whattrigger is likely to care? Unfortunately, that argument seems too
weak.
Does anyone have a cleaner idea for keeping track of the system column
values or a stronger argument for not bothering?Regarding to the first suggestion,
I think, it is better not to care about system columns on foreign
tables,
because it fully depends on driver's implementation whether FDW fetches
"ctid" from its data source, or not.
Usually, system columns of foreign table, except for tableoid, are
nonsense.Because of implementation reason, postgres_fdw fetches "ctid" of
remote tables on UPDATE / DELETE, it is not a common nature for all FDW
drivers. For example, we can assume an implementation that uses primary
key
of remote table to identify the record to be updated or deleted. In this
case, local "ctid" does not have meaningful value.
So, fundamentally, we cannot guarantee FDW driver returns meaningful
"ctid"
or other system columns.I agree, I think it is somewhat clunky to have postgres_fdw use a feature
that is basically meaningless for other FDWs. Looking at some threads in
this list, it confused many people.My own reasoning for accepting omission of system columns is more along the
lines of Robert's argument. Regardless, three folks voting to do so and
none against suffices for me. I documented the system columns limitation,
made the returningList change I mentioned, and committed the patch.
Thank you, I'm glad the patch found its way to the repository !
This is off-topic, but maybe we could devise an API allowing for local
"system attributes" on foreign table. This would allow FDWs to carry
attributes that weren't declared as part of the table definition. This
could then be used for postgres_fdw ctid, as well as others foreign data
wrappers equivalent of an implicit "tuple id".We could, but I discourage it. System columns are a legacy feature; I doubt
we'd choose that design afresh today. On the off-chance that you need the
value of a remote system column, you can already declare an ordinary
foreign table column for it. I raised the issue because it's inconsistent
for RETURNING to convey system columns while tg_trigtuple/tg_newtuple do
not, not because acquiring system columns from foreign tables is notably
useful.
The idea here was to allow an FDW author to add columns which are not part of
the table definition, for example colums which are required to identify the
tuple remotely. Without system columns, a postgres_fdw user would have to
declare the ctid column on every table for a tuble to be identifiable. The
proposal would allow postgres_fdw to automatically inject an hidden (system ?)
column on every table for this ctid.
--
Ronan Dunklau
http://dalibo.com - http://dalibo.org