diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml index 950245d..e8437ac 100644 *** a/doc/src/sgml/trigger.sgml --- b/doc/src/sgml/trigger.sgml *************** *** 212,218 **** change the data returned by INSERT RETURNING or UPDATE RETURNING, and is useful when the view will not show exactly the same data ! that was provided. --- 212,220 ---- change the data returned by INSERT RETURNING or UPDATE RETURNING, and is useful when the view will not show exactly the same data ! that was provided. Likewise, for DELETE operations the ! OLD variable can be modified before returning it, and ! the changes will be reflected in the output data. diff --git a/src/backend/commandindex b502941..645c216 100644 *** a/src/backend/commands/trigger.c --- b/src/backend/commands/trigger.c *************** *** 2654,2666 **** ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, } } ! bool ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ! HeapTuple trigtuple) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; TriggerData LocTriggerData; ! HeapTuple rettuple; int i; LocTriggerData.type = T_TriggerData; --- 2654,2666 ---- } } ! TupleTableSlot * ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ! HeapTuple trigtuple, TupleTableSlot *slot) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; TriggerData LocTriggerData; ! HeapTuple rettuple = trigtuple; int i; LocTriggerData.type = T_TriggerData; *************** *** 2694,2704 **** ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, relinfo->ri_TrigInstrument, GetPerTupleMemoryContext(estate)); if (rettuple == NULL) ! return false; /* Delete was suppressed */ ! if (rettuple != trigtuple) ! heap_freetuple(rettuple); } ! return true; } void --- 2694,2720 ---- relinfo->ri_TrigInstrument, GetPerTupleMemoryContext(estate)); if (rettuple == NULL) ! return NULL; /* Delete was suppressed */ } ! ! if (rettuple != trigtuple) ! { ! /* ! * Return the modified tuple using the es_trig_tuple_slot. We assume ! * the tuple was allocated in per-tuple memory context, and therefore ! * will go away by itself. The tuple table slot should not try to ! * clear it. ! */ ! TupleTableSlot *newslot = estate->es_trig_tuple_slot; ! TupleDesc tupdesc = RelationGetDescr(relinfo->ri_RelationDesc); ! ! if (newslot->tts_tupleDescriptor != tupdesc) ! ExecSetSlotDescriptor(newslot, tupdesc); ! ExecStoreTuple(rettuple, newslot, InvalidBuffer, false); ! slot = newslot; ! } ! ! return slot; } void diff --git a/src/backend/executor/nodindex 30add8e..d81c5a4 100644 *** a/src/backend/executor/nodeModifyTable.c --- b/src/backend/executor/nodeModifyTable.c *************** *** 704,716 **** ExecDelete(ModifyTableState *mtstate, if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->trig_delete_instead_row) { ! bool dodelete; ! Assert(oldtuple != NULL); ! dodelete = ExecIRDeleteTriggers(estate, resultRelInfo, oldtuple); ! if (!dodelete) /* "do nothing" */ return NULL; } else if (resultRelInfo->ri_FdwRoutine) { --- 704,726 ---- if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->trig_delete_instead_row) { ! /* ! * Store the heap tuple into the tuple table slot, making sure we have a ! * writable copy. We can use the trigger tuple slot. ! */ ! slot = estate->es_trig_tuple_slot; ! if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc)) ! ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc)); ! ExecStoreTuple(oldtuple, slot, InvalidBuffer, false); ! oldtuple = ExecMaterializeSlot(slot); ! slot = ExecIRDeleteTriggers(estate, resultRelInfo, oldtuple, slot); ! if (slot == NULL) /* "do nothing" */ return NULL; + + /* trigger might have changed tuple */ + oldtuple = ExecMaterializeSlot(slot); } else if (resultRelInfo->ri_FdwRoutine) { *************** *** 851,864 **** ldelete:; /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) { - /* - * We have to put the target tuple into a slot, which means first we - * gotta fetch it. We can use the trigger tuple slot. - */ TupleTableSlot *rslot; HeapTupleData deltuple; Buffer delbuffer; if (resultRelInfo->ri_FdwRoutine) { /* FDW must have provided a slot containing the deleted row */ --- 861,882 ---- /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) { TupleTableSlot *rslot; HeapTupleData deltuple; Buffer delbuffer; + /* + * If we fired an INSTEAD OF trigger, we should use the tuple returned + * from said trigger for the RETURNING projections. + */ + if (resultRelInfo->ri_TrigDesc && + resultRelInfo->ri_TrigDesc->trig_delete_instead_row) + return ExecProcessReturning(resultRelInfo, slot, planSlot); + + /* + * Otherwise we have to to fetch the target tuple into a slot. We can + * use the trigger tuple slot here as well. + */ if (resultRelInfo->ri_FdwRoutine) { /* FDW must have provided a slot containing the deleted row */ diff --git a/src/include/commands/trigger.h bindex 36c1134..8582177 100644 *** a/src/include/commands/trigger.h --- b/src/include/commands/trigger.h *************** *** 209,217 **** extern void ExecARDeleteTriggers(EState *estate, ItemPointer tupleid, HeapTuple fdw_trigtuple, TransitionCaptureState *transition_capture); ! extern bool ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ! HeapTuple trigtuple); extern void ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo); extern void ExecASUpdateTriggers(EState *estate, --- 209,218 ---- ItemPointer tupleid, HeapTuple fdw_trigtuple, TransitionCaptureState *transition_capture); ! extern TupleTableSlot *ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ! HeapTuple trigtuple, ! TupleTableSlot *slot); extern void ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo); extern void ExecASUpdateTriggers(EState *estate, diff --git a/src/test/regress/expecteindex ac132b0..b445e26 100644 *** a/src/test/regress/expected/triggers.out --- b/src/test/regress/expected/triggers.out *************** *** 1309,1314 **** UPDATE 0 --- 1309,1341 ---- DELETE FROM european_city_view; DELETE 0 \set QUIET true + -- modifying RETURNING from INSTEAD OF triggers on DELETEs + CREATE VIEW instead_of_delete_returning AS SELECT 1 AS fff; + CREATE FUNCTION instead_of_delete_returning_f() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + RETURN NULL; + END; + $$; + CREATE TRIGGER instead_of_delete_returning_t INSTEAD OF DELETE ON instead_of_delete_returning + FOR EACH ROW EXECUTE PROCEDURE instead_of_delete_returning_f(); + DELETE FROM instead_of_delete_returning RETURNING *; + fff + ----- + (0 rows) + + CREATE OR REPLACE FUNCTION instead_of_delete_returning_f() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + OLD.fff := 3; + RETURN OLD; + END; + $$; + DELETE FROM instead_of_delete_returning RETURNING *; + fff + ----- + 3 + (1 row) + + DROP VIEW instead_of_delete_returning CASCADE; -- rules bypassing no-op triggers CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view DO INSTEAD INSERT INTO city_view diff --git a/src/test/regress/sql/triggers.sqindex b10159a..636e387 100644 *** a/src/test/regress/sql/triggers.sql --- b/src/test/regress/sql/triggers.sql *************** *** 916,921 **** DELETE FROM european_city_view; --- 916,941 ---- \set QUIET true + -- modifying RETURNING from INSTEAD OF triggers on DELETEs + CREATE VIEW instead_of_delete_returning AS SELECT 1 AS fff; + CREATE FUNCTION instead_of_delete_returning_f() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + RETURN NULL; + END; + $$; + CREATE TRIGGER instead_of_delete_returning_t INSTEAD OF DELETE ON instead_of_delete_returning + FOR EACH ROW EXECUTE PROCEDURE instead_of_delete_returning_f(); + + DELETE FROM instead_of_delete_returning RETURNING *; + CREATE OR REPLACE FUNCTION instead_of_delete_returning_f() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + OLD.fff := 3; + RETURN OLD; + END; + $$; + DELETE FROM instead_of_delete_returning RETURNING *; + DROP VIEW instead_of_delete_returning CASCADE; + -- rules bypassing no-op triggers CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view DO INSTEAD INSERT INTO city_view