WIP: Triggers on VIEWs
Here is a WIP patch implementing triggers on VIEWs, as outlined in the
proof-of-concept here:
http://archives.postgresql.org/pgsql-hackers/2010-08/msg00160.php
The new triggers allowed on a VIEW are:
1). Statement-level BEFORE INSERT/UPDATE/DELETE
2). Row-level INSTEAD OF INSERT/UPDATE/DELETE
3). Statement-level AFTER INSERT/UPDATE/DELETE
The new INSTEAD OF trigger type may only be used with VIEWs, and may
only be row-level. It does not support the WHEN or FOR UPDATE OF
column_list options.
There are still a number of things left todo:
- extend ALTER VIEW with enable/disable trigger commands
- much more testing
- documentation
and then there's the question of what to do about the concurrency
issues raised by Marko Tiikkaja. Currently it works like Oracle (i.e.,
no locking).
Regards,
Dean
Attachments:
view_triggers.patchapplication/octet-stream; name=view_triggers.patchDownload
*** ./contrib/spi/autoinc.c.orig 2010-07-31 12:06:47.000000000 +0100
--- ./contrib/spi/autoinc.c 2010-08-15 12:55:08.000000000 +0100
***************
*** 38,44 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
--- 38,44 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
*** ./contrib/spi/insert_username.c.orig 2010-07-31 12:07:44.000000000 +0100
--- ./contrib/spi/insert_username.c 2010-08-15 12:56:59.000000000 +0100
***************
*** 41,47 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
--- 41,47 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
*** ./contrib/spi/moddatetime.c.orig 2010-07-31 12:10:47.000000000 +0100
--- ./contrib/spi/moddatetime.c 2010-08-15 12:57:35.000000000 +0100
***************
*** 47,53 ****
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
--- 47,53 ----
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
*** ./contrib/spi/timetravel.c.orig 2010-07-31 12:13:01.000000000 +0100
--- ./contrib/spi/timetravel.c 2010-08-15 16:42:48.000000000 +0100
***************
*** 122,128 ****
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
--- 122,128 ----
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
*** ./src/backend/catalog/index.c.orig 2010-08-14 23:52:07.000000000 +0100
--- ./src/backend/catalog/index.c 2010-08-15 09:53:21.000000000 +0100
***************
*** 825,830 ****
--- 825,831 ----
trigger->funcname = SystemFuncName("unique_key_recheck");
trigger->args = NIL;
trigger->before = false;
+ trigger->instead = false;
trigger->row = true;
trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE;
trigger->columns = NIL;
*** ./src/backend/commands/tablecmds.c.orig 2010-08-14 23:54:39.000000000 +0100
--- ./src/backend/commands/tablecmds.c 2010-08-15 09:54:57.000000000 +0100
***************
*** 5605,5610 ****
--- 5605,5611 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = myRel;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
/* Either ON INSERT or ON UPDATE */
***************
*** 5668,5673 ****
--- 5669,5675 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_DELETE;
fk_trigger->columns = NIL;
***************
*** 5721,5726 ****
--- 5723,5729 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_UPDATE;
fk_trigger->columns = NIL;
*** ./src/backend/commands/trigger.c.orig 2010-08-14 23:58:52.000000000 +0100
--- ./src/backend/commands/trigger.c 2010-08-15 10:22:19.000000000 +0100
***************
*** 150,159 ****
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 150,198 ----
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind == RELKIND_RELATION)
! {
! /* Forbid INSTEAD OF triggers on tables */
! if (stmt->instead)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a table",
! RelationGetRelationName(rel)),
! errhint("Define INSTEAD OF triggers on views")));
! }
! else if (rel->rd_rel->relkind == RELKIND_VIEW)
! {
! /* Only allow INSTEAD OF or STATEMENT-level triggers on views */
! if (!stmt->instead && stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a view",
! RelationGetRelationName(rel)),
! errhint("Define ROW-level BEFORE and AFTER triggers on tables")));
!
! /* Disallow STATEMENT-level INSTEAD OF triggers */
! if (stmt->instead && !stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("STATEMENT-level INSTEAD OF triggers are not supported"),
! errhint("Use FOR EACH ROW for INSTEAD OF triggers")));
!
! /* Disallow WHEN clauses with INSTEAD OF triggers */
! if (stmt->instead && stmt->whenClause)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a WHEN condition with an INSTEAD OF trigger")));
!
! /* Disallow column selection with INSTEAD OF triggers */
! if (stmt->instead && stmt->columns != NIL)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a column list with an INSTEAD OF trigger")));
! }
! else
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
***************
*** 188,193 ****
--- 227,234 ----
TRIGGER_CLEAR_TYPE(tgtype);
if (stmt->before)
TRIGGER_SETT_BEFORE(tgtype);
+ if (stmt->instead)
+ TRIGGER_SETT_INSTEAD(tgtype);
if (stmt->row)
TRIGGER_SETT_ROW(tgtype);
tgtype |= stmt->events;
***************
*** 198,203 ****
--- 239,252 ----
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("TRUNCATE FOR EACH ROW triggers are not supported")));
+ /* Disallow TRUNCATE triggers on VIEWs */
+ if (rel->rd_rel->relkind == RELKIND_VIEW && TRIGGER_FOR_TRUNCATE(tgtype))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is a view",
+ RelationGetRelationName(rel)),
+ errhint("Define TRUNCATE triggers on tables")));
+
/*
* Parse the WHEN clause, if any
*/
***************
*** 1031,1040 ****
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 1080,1090 ----
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION &&
! rel->rd_rel->relkind != RELKIND_VIEW)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
***************
*** 1506,1511 ****
--- 1556,1566 ----
n = trigdesc->n_before_row;
t = trigdesc->tg_before_row;
}
+ else if (TRIGGER_FOR_INSTEAD(trigger->tgtype))
+ {
+ n = trigdesc->n_instead_row;
+ t = trigdesc->tg_instead_row;
+ }
else
{
n = trigdesc->n_after_row;
***************
*** 1656,1661 ****
--- 1711,1729 ----
else
t[i] = NULL;
}
+ n = newdesc->n_instead_row;
+ t = newdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ {
+ if (n[i] > 0)
+ {
+ tnew = (int *) palloc(n[i] * sizeof(int));
+ memcpy(tnew, t[i], n[i] * sizeof(int));
+ t[i] = tnew;
+ }
+ else
+ t[i] = NULL;
+ }
n = newdesc->n_after_statement;
t = newdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
***************
*** 1698,1703 ****
--- 1766,1775 ----
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
pfree(t[i]);
+ t = trigdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ if (t[i] != NULL)
+ pfree(t[i]);
t = trigdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
***************
*** 2000,2005 ****
--- 2072,2121 ----
true, NULL, trigtuple, recheckIndexes, NULL);
}
+ HeapTuple
+ ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_INSERT];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_INSERT];
+ TriggerData LocTriggerData;
+ HeapTuple newtuple = trigtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, NULL, trigtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != trigtuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
{
***************
*** 2134,2139 ****
--- 2250,2299 ----
}
}
+ HeapTuple
+ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_DELETE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_DELETE];
+ TriggerData LocTriggerData;
+ HeapTuple oldtuple = NULL;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, trigtuple, NULL))
+ continue;
+
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != oldtuple && oldtuple != NULL)
+ heap_freetuple(oldtuple);
+ oldtuple = rettuple;
+ if (oldtuple == NULL)
+ break;
+ }
+ return oldtuple;
+ }
+
void
ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
{
***************
*** 2281,2286 ****
--- 2441,2490 ----
}
}
+ HeapTuple
+ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple oldtuple, HeapTuple newtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_UPDATE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_UPDATE];
+ TriggerData LocTriggerData;
+ HeapTuple intuple = newtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, oldtuple, newtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = oldtuple;
+ LocTriggerData.tg_newtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != intuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
{
*** ./src/backend/executor/execMain.c.orig 2010-08-15 00:04:31.000000000 +0100
--- ./src/backend/executor/execMain.c 2010-08-15 10:01:37.000000000 +0100
***************
*** 899,908 ****
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("cannot change view \"%s\"",
! RelationGetRelationName(resultRelationDesc))));
break;
default:
ereport(ERROR,
--- 899,905 ----
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! /* OK */
break;
default:
ereport(ERROR,
*** ./src/backend/executor/nodeModifyTable.c.orig 2010-07-28 09:37:15.000000000 +0100
--- ./src/backend/executor/nodeModifyTable.c 2010-08-15 15:19:54.000000000 +0100
***************
*** 149,154 ****
--- 149,200 ----
}
/* ----------------------------------------------------------------
+ * ExecCheckViewTriggers
+ *
+ * If the result relation is a view, check that it has the
+ * appropriate INSTEAD OF triggers to carry out the current
+ * operation.
+ * ----------------------------------------------------------------
+ */
+ static void
+ ExecCheckViewTriggers(ResultRelInfo *resultRelInfo,
+ CmdType operation)
+ {
+ TriggerDesc *trigDesc = resultRelInfo->ri_TrigDesc;
+
+ switch (operation)
+ {
+ case CMD_INSERT:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_INSERT] == 0)
+ ereport(ERROR,
+ (errmsg("cannot insert into a view"),
+ errhint("You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.")));
+ break;
+
+ case CMD_UPDATE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_UPDATE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot update a view"),
+ errhint("You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.")));
+ break;
+
+ case CMD_DELETE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_DELETE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot delete from a view"),
+ errhint("You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.")));
+ break;
+
+ default:
+ elog(ERROR, "unknown operation");
+ break;
+ }
+ }
+
+ /* ----------------------------------------------------------------
* ExecInsert
*
* For INSERT, we have to insert the tuple into the target relation
***************
*** 167,172 ****
--- 213,219 ----
Relation resultRelationDesc;
Oid newId;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* get the heap tuple out of the tuple table slot, making sure we have a
***************
*** 179,184 ****
--- 226,232 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
/*
* If the result relation has OIDs, force the tuple's OID to zero so that
***************
*** 195,202 ****
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /* BEFORE ROW INSERT Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
--- 243,258 ----
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /*
! * BEFORE ROW INSERT Triggers.
! *
! * We disallow BEFORE ROW triggers on views. Technically we could allow
! * them for INSERT, but we can't do them for UPDATE or DELETE because we
! * won't have a tupleid, so we disallow them for INSERT as well for
! * consistency.
! */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
***************
*** 237,244 ****
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
(estate->es_processed)++;
estate->es_lastoid = newId;
--- 293,334 ----
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! if (resultRelIsView)
! {
! /* INSTEAD OF trigger(s) do the actual insert */
! HeapTuple newtuple;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_INSERT);
! newtuple = ExecIRInsertTriggers(estate, resultRelInfo, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. 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(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! newId = InvalidOid;
! }
! else
! {
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
! }
(estate->es_processed)++;
estate->es_lastoid = newId;
***************
*** 251,258 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW INSERT Triggers */
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
--- 341,353 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /*
! * AFTER ROW INSERT Triggers
! *
! * As with BEFORE ROW triggers, we disallow these on views.
! */
! if (!resultRelIsView)
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
***************
*** 268,280 ****
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
--- 363,381 ----
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed.
! *
! * When deleting from a table, tupleid identifies the tuple to
! * 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.
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
***************
*** 284,298 ****
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW DELETE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
--- 385,407 ----
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
+ bool resultRelIsView;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /*
! * BEFORE ROW DELETE Triggers
! *
! * We disallow BEFORE ROW triggers on views because we don't have a
! * tupleid.
! */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
***************
*** 313,323 ****
* referential integrity updates in serializable transactions.
*/
ldelete:;
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 422,472 ----
* referential integrity updates in serializable transactions.
*/
ldelete:;
! if (resultRelIsView)
! {
! /* INSTEAD OF trigger(s) do the actual delete */
! HeapTupleData tuple;
! HeapTuple deltuple;
!
! tuple.t_data = oldtuple;
! tuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(tuple.t_self));
! tuple.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_DELETE);
! deltuple = ExecIRDeleteTriggers(estate, resultRelInfo, &tuple);
!
! if (deltuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (deltuple != &tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. 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 *slot = estate->es_trig_tuple_slot;
! TupleDesc tupdesc = RelationGetDescr(resultRelationDesc);
!
! if (slot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(slot, tupdesc);
! ExecStoreTuple(deltuple, slot, InvalidBuffer, false);
! oldtuple = deltuple->t_data;
! }
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
***************
*** 367,374 ****
* anyway, since the tuple is still visible to other transactions.
*/
! /* AFTER ROW DELETE Triggers */
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 516,528 ----
* anyway, since the tuple is still visible to other transactions.
*/
! /*
! * AFTER ROW DELETE Triggers
! *
! * As with BEFORE ROW triggers, we disallow these on views.
! */
! if (!resultRelIsView)
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
***************
*** 382,391 ****
HeapTupleData deltuple;
Buffer delbuffer;
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
--- 536,555 ----
HeapTupleData deltuple;
Buffer delbuffer;
! if (resultRelIsView)
! {
! deltuple.t_data = oldtuple;
! deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(deltuple.t_self));
! deltuple.t_tableOid = InvalidOid;
! }
! else
! {
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
! }
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
***************
*** 395,401 ****
slot, planSlot);
ExecClearTuple(slot);
! ReleaseBuffer(delbuffer);
return rslot;
}
--- 559,566 ----
slot, planSlot);
ExecClearTuple(slot);
! if (!resultRelIsView)
! ReleaseBuffer(delbuffer);
return rslot;
}
***************
*** 413,423 ****
--- 578,594 ----
* is, we don't want to get stuck in an infinite loop
* which corrupts your database..
*
+ * When updating a table, tupleid identifies the tuple to
+ * 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.
+ *
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecUpdate(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EPQState *epqstate,
***************
*** 430,435 ****
--- 601,607 ----
ItemPointerData update_ctid;
TransactionId update_xmax;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* abort the operation if not running transactions
***************
*** 448,456 ****
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW UPDATE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
--- 620,635 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /*
! * BEFORE ROW UPDATE Triggers
! *
! * We disallow BEFORE ROW triggers on views because we don't have a
! * tupleid.
! */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
***************
*** 501,511 ****
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in serializable transactions.
*/
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 680,732 ----
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in serializable transactions.
*/
! if (resultRelIsView)
! {
! /* INSTEAD OF trigger(s) do the actual update */
! HeapTupleData oldtup;
! HeapTuple newtuple;
!
! oldtup.t_data = oldtuple;
! oldtup.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(oldtup.t_self));
! oldtup.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_UPDATE);
! newtuple = ExecIRUpdateTriggers(estate, resultRelInfo,
! &oldtup, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. 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(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
***************
*** 568,576 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW UPDATE Triggers */
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
--- 789,802 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /*
! * AFTER ROW UPDATE Triggers
! *
! * As with BEFORE ROW triggers, we disallow these on views.
! */
! if (!resultRelIsView)
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
***************
*** 654,659 ****
--- 880,886 ----
TupleTableSlot *planSlot;
ItemPointer tupleid = NULL;
ItemPointerData tuple_ctid;
+ HeapTupleHeader oldtuple;
/*
* On first call, fire BEFORE STATEMENT triggers before proceeding.
***************
*** 705,727 ****
if (junkfilter != NULL)
{
/*
! * extract the 'ctid' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! /* shouldn't ever get a null result... */
! if (isNull)
! elog(ERROR, "ctid is NULL");
!
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
}
/*
--- 932,968 ----
if (junkfilter != NULL)
{
+ ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
+ Relation resultRel = resultRelInfo->ri_RelationDesc;
+
/*
! * extract the 'ctid' or 'wholerow' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "wholerow is NULL");
!
! oldtuple = DatumGetHeapTupleHeader(datum);
! }
! else
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "ctid is NULL");
!
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
! }
}
/*
***************
*** 737,747 ****
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, planSlot,
&node->mt_epqstate, estate);
break;
default:
--- 978,988 ----
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, oldtuple, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, oldtuple, planSlot,
&node->mt_epqstate, estate);
break;
default:
***************
*** 825,830 ****
--- 1066,1075 ----
i = 0;
foreach(l, node->plans)
{
+ Relation resultRel = estate->es_result_relation_info->ri_RelationDesc;
+ if (resultRel->rd_rel->relkind == RELKIND_VIEW)
+ ExecCheckViewTriggers(estate->es_result_relation_info, operation);
+
subplan = (Plan *) lfirst(l);
mtstate->mt_plans[i] = ExecInitNode(subplan, estate, eflags);
estate->es_result_relation_info++;
***************
*** 980,989 ****
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /* For UPDATE/DELETE, find the ctid junk attr now */
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
}
resultRelInfo->ri_junkFilter = j;
--- 1225,1248 ----
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /*
! * For UPDATE/DELETE of real tables, find the ctid junk
! * attr now. For VIEWs use the wholerow junk attr.
! */
! Relation resultRel = resultRelInfo->ri_RelationDesc;
!
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk wholerow column");
! }
! else
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
! }
}
resultRelInfo->ri_junkFilter = j;
*** ./src/backend/nodes/copyfuncs.c.orig 2010-08-15 00:06:57.000000000 +0100
--- ./src/backend/nodes/copyfuncs.c 2010-08-15 09:56:30.000000000 +0100
***************
*** 3227,3232 ****
--- 3227,3233 ----
COPY_NODE_FIELD(funcname);
COPY_NODE_FIELD(args);
COPY_SCALAR_FIELD(before);
+ COPY_SCALAR_FIELD(instead);
COPY_SCALAR_FIELD(row);
COPY_SCALAR_FIELD(events);
COPY_NODE_FIELD(columns);
*** ./src/backend/nodes/equalfuncs.c.orig 2010-08-15 00:09:01.000000000 +0100
--- ./src/backend/nodes/equalfuncs.c 2010-08-15 09:57:10.000000000 +0100
***************
*** 1685,1690 ****
--- 1685,1691 ----
COMPARE_NODE_FIELD(funcname);
COMPARE_NODE_FIELD(args);
COMPARE_SCALAR_FIELD(before);
+ COMPARE_SCALAR_FIELD(instead);
COMPARE_SCALAR_FIELD(row);
COMPARE_SCALAR_FIELD(events);
COMPARE_NODE_FIELD(columns);
*** ./src/backend/optimizer/plan/planmain.c.orig 2010-07-28 09:37:12.000000000 +0100
--- ./src/backend/optimizer/plan/planmain.c 2010-08-15 09:29:08.000000000 +0100
***************
*** 180,185 ****
--- 180,194 ----
add_base_rels_to_query(root, (Node *) parse->jointree);
/*
+ * If the query target is a VIEW, it won't be in the jointree, but we
+ * need a dummy RelOptInfo node for it. This need not have any stats in
+ * it because it always just goes at the top of the plan tree.
+ */
+ if (parse->resultRelation &&
+ root->simple_rel_array[parse->resultRelation] == NULL)
+ build_simple_rel(root, parse->resultRelation, RELOPT_OTHER_MEMBER_REL);
+
+ /*
* Examine the targetlist and qualifications, adding entries to baserel
* targetlists for all referenced Vars. Restrict and join clauses are
* added to appropriate lists belonging to the mentioned relations. We
*** ./src/backend/optimizer/prep/preptlist.c.orig 2010-07-28 09:37:12.000000000 +0100
--- ./src/backend/optimizer/prep/preptlist.c 2010-08-15 09:35:45.000000000 +0100
***************
*** 38,44 ****
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table);
/*
--- 38,44 ----
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel);
/*
***************
*** 52,57 ****
--- 52,58 ----
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
+ bool result_is_view = false;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
***************
*** 63,81 ****
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
}
/*
! * for heap_form_tuple to work, the targetlist must match the exact order
! * of the attributes. We also need to fill in any missing attributes. -ay
! * 10/94
*/
! if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
! tlist = expand_targetlist(tlist, command_type,
! result_relation, range_table);
/*
* for "update" and "delete" queries, add ctid of the result relation into
--- 64,100 ----
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
+ Relation rel;
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
+
+ /*
+ * Open the result relation. We assume that the rewriter already
+ * acquired at least AccessShareLock on it, so we need no lock here.
+ */
+ rel = heap_open(getrelid(result_relation, range_table), NoLock);
+ result_is_view = rel->rd_rel->relkind == RELKIND_VIEW;
+
+ /*
+ * for heap_form_tuple to work, the targetlist must match the exact
+ * order of the attributes. We also need to fill in any missing
+ * attributes. -ay 10/94
+ */
+ if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ tlist = expand_targetlist(tlist, command_type,
+ result_relation, rel);
+
+ heap_close(rel, NoLock);
}
/*
! * For an UPDATE, expand_targetlist already created a fresh tlist. For
! * DELETE, better do a listCopy so that we don't destructively modify
! * the original tlist (is this really necessary?).
*/
! if (command_type == CMD_DELETE)
! tlist = list_copy(tlist);
/*
* for "update" and "delete" queries, add ctid of the result relation into
***************
*** 83,90 ****
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
*/
! if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
{
TargetEntry *tle;
Var *var;
--- 102,114 ----
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
+ *
+ * We don't do this if the result relation is a view, since that won't
+ * expose a ctid. The rewriter should have already added a wholerow TLE
+ * for the view's subselect.
*/
! if (!result_is_view &&
! (command_type == CMD_UPDATE || command_type == CMD_DELETE))
{
TargetEntry *tle;
Var *var;
***************
*** 97,110 ****
pstrdup("ctid"),
true);
- /*
- * For an UPDATE, expand_targetlist already created a fresh tlist. For
- * DELETE, better do a listCopy so that we don't destructively modify
- * the original tlist (is this really necessary?).
- */
- if (command_type == CMD_DELETE)
- tlist = list_copy(tlist);
-
tlist = lappend(tlist, tle);
}
--- 121,126 ----
***************
*** 169,187 ****
}
else
{
! /* Not a table, so we need the whole row as a junk var */
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
}
}
--- 185,227 ----
}
else
{
! bool exists = false;
! ListCell *l;
!
! /*
! * Not a table, so we need the whole row as a junk var. If the
! * query target is a view, the rewriter will have already added
! * a whole row var, so don't add another for that RTE.
! */
! foreach(l, tlist)
! {
! TargetEntry *tle = (TargetEntry *) lfirst(l);
!
! if (tle->resjunk &&
! IsA(tle->expr, Var) &&
! ((Var *) tle->expr)->varno == rc->rti &&
! strcmp(tle->resname, "wholerow") == 0)
! {
! exists = true;
! break;
! }
! }
!
! if (!exists)
! {
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
! }
}
}
***************
*** 241,251 ****
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
- Relation rel;
int attrno,
numattrs;
--- 281,290 ----
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel)
{
List *new_tlist = NIL;
ListCell *tlist_item;
int attrno,
numattrs;
***************
*** 256,267 ****
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order. We assume
! * that the rewriter already acquired at least AccessShareLock on the
! * relation, so we need no lock here.
*/
- rel = heap_open(getrelid(result_relation, range_table), NoLock);
-
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
--- 295,302 ----
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order.
*/
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
***************
*** 399,406 ****
tlist_item = lnext(tlist_item);
}
- heap_close(rel, NoLock);
-
return new_tlist;
}
--- 434,439 ----
*** ./src/backend/optimizer/util/plancat.c.orig 2010-07-28 09:37:11.000000000 +0100
--- ./src/backend/optimizer/util/plancat.c 2010-08-15 09:19:26.000000000 +0100
***************
*** 441,447 ****
*tuples = 1;
break;
default:
! /* else it has no disk storage; probably shouldn't get here? */
*pages = 0;
*tuples = 0;
break;
--- 441,452 ----
*tuples = 1;
break;
default:
! /*
! * Else it has no disk storage; probably shouldn't get here,
! * unless this is a VIEW which is the query target, in which
! * case we don't care about the sizes, since it will always be
! * at the top of the plan tree.
! */
*pages = 0;
*tuples = 0;
break;
*** ./src/backend/parser/gram.y.orig 2010-08-15 00:11:26.000000000 +0100
--- ./src/backend/parser/gram.y 2010-08-15 09:48:07.000000000 +0100
***************
*** 245,251 ****
%type <str> OptSchemaName
%type <list> OptSchemaEltList
! %type <boolean> TriggerActionTime TriggerForSpec opt_trusted opt_restart_seqs
%type <list> TriggerEvents TriggerOneEvent
%type <value> TriggerFuncArg
--- 245,252 ----
%type <str> OptSchemaName
%type <list> OptSchemaEltList
! %type <boolean> TriggerForSpec opt_trusted opt_restart_seqs
! %type <ival> TriggerActionTime
%type <list> TriggerEvents TriggerOneEvent
%type <value> TriggerFuncArg
***************
*** 3375,3381 ****
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = $4;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
--- 3376,3383 ----
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = ($4 & 1) != 0;
! n->instead = ($4 & 2) != 0;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
***************
*** 3397,3402 ****
--- 3399,3405 ----
n->funcname = $17;
n->args = $19;
n->before = FALSE;
+ n->instead = FALSE;
n->row = TRUE;
n->events = intVal(linitial($6));
n->columns = (List *) lsecond($6);
***************
*** 3410,3417 ****
;
TriggerActionTime:
! BEFORE { $$ = TRUE; }
! | AFTER { $$ = FALSE; }
;
TriggerEvents:
--- 3413,3421 ----
;
TriggerActionTime:
! BEFORE { $$ = 1; }
! | AFTER { $$ = 0; }
! | INSTEAD OF { $$ = 2; }
;
TriggerEvents:
*** ./src/backend/rewrite/rewriteHandler.c.orig 2010-07-28 09:37:27.000000000 +0100
--- ./src/backend/rewrite/rewriteHandler.c 2010-08-15 09:07:20.000000000 +0100
***************
*** 564,570 ****
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
--- 564,572 ----
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions. For an UPDATE on a VIEW, add tlist entries assigning any
! * unassigned attributes their current values. The RHS of such assignments
! * will be rewritten to refer to the view's subselect node.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
***************
*** 724,729 ****
--- 726,753 ----
false);
}
+ /*
+ * For an UPDATE on a VIEW where the TLE is missing, assign the
+ * current value, which will be rewritten as a query from the view's
+ * subselect node by ApplyRetrieveRule().
+ */
+ if (new_tle == NULL && commandType == CMD_UPDATE &&
+ target_relation->rd_rel->relkind == RELKIND_VIEW)
+ {
+ Node *new_expr;
+
+ new_expr = (Node *) makeVar(parsetree->resultRelation,
+ attrno,
+ att_tup->atttypid,
+ att_tup->atttypmod,
+ 0);
+
+ new_tle = makeTargetEntry((Expr *) new_expr,
+ attrno,
+ pstrdup(NameStr(att_tup->attname)),
+ false);
+ }
+
if (new_tle)
new_tlist = lappend(new_tlist, new_tle);
}
***************
*** 1149,1154 ****
--- 1173,1180 ----
RangeTblEntry *rte,
*subrte;
RowMarkClause *rc;
+ Var *var;
+ TargetEntry *tle;
if (list_length(rule->actions) != 1)
elog(ERROR, "expected just one rule action");
***************
*** 1177,1220 ****
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! /*
! * VIEWs are really easy --- just plug the view query in as a subselect,
! * replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! /*
! * We move the view's permission check data down to its rangetable. The
! * checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
!
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as implicit
! * FOR UPDATE/SHARE, the same as the parser would have done if the view's
! * subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will be
! * made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
return parsetree;
}
--- 1203,1313 ----
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! if (rt_index == parsetree->resultRelation)
! {
! /*
! * We have a VIEW as the query target. For DELETE and UPDATE, we
! * add a new subselect RTE using the view's query and adjust any
! * VARs in the original query to point to this instead of the
! * original view RTE.
! *
! * We keep the original view in the rtable as the query target, and
! * any Vars in the returning list that reference it are left alone.
! *
! * The resulting jointree fromlist will not refer to the view RTE,
! * and so the planner won't try to join to it. This will result in
! * a plan with a ModifyTable node at the root, referring to the
! * original view relation, and a subselect based on the view's query
! * merged with any user conditions.
! *
! * For INSERTS we do nothing. The original view remains the query
! * target.
! */
! if (parsetree->commandType == CMD_DELETE ||
! parsetree->commandType == CMD_UPDATE)
! {
! List *returningList = parsetree->returningList;
! /*
! * Make a new subselect RTE from the view query and adjust the
! * the original query to point to this instead of the original
! * view, while preserving the view resultRelation and any
! * returningList Vars.
! */
! rte = copyObject(rt_fetch(rt_index, parsetree->rtable));
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! parsetree->rtable = lappend(parsetree->rtable, rte);
! parsetree->returningList = NIL;
!
! ChangeVarNodes((Node *) parsetree, rt_index,
! list_length(parsetree->rtable), 0);
!
! parsetree->resultRelation = rt_index;
! parsetree->returningList = returningList;
!
! /*
! * Add a "wholerow" junk TLE so that the executor can retrieve
! * the old VIEW tuples to pass to the INSTEAD OF triggers.
! */
! var = makeVar(list_length(parsetree->rtable),
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
!
! tle = makeTargetEntry((Expr *) var,
! list_length(parsetree->targetList) + 1,
! pstrdup("wholerow"),
! true);
! parsetree->targetList = lappend(parsetree->targetList, tle);
! }
! }
! else
! {
! /*
! * Selecting from the VIEW --- just plug the view query in as a
! * subselect, replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
!
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
!
! /*
! * We move the view's permission check data down to its rangetable.
! * The checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
!
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
!
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as
! * implicit FOR UPDATE/SHARE, the same as the parser would have done
! * if the view's subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will
! * be made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
! }
return parsetree;
}
*** ./src/backend/utils/adt/ruleutils.c.orig 2010-08-15 00:14:30.000000000 +0100
--- ./src/backend/utils/adt/ruleutils.c 2010-08-15 10:45:04.000000000 +0100
***************
*** 545,550 ****
--- 545,552 ----
if (TRIGGER_FOR_BEFORE(trigrec->tgtype))
appendStringInfo(&buf, "BEFORE");
+ else if (TRIGGER_FOR_INSTEAD(trigrec->tgtype))
+ appendStringInfo(&buf, "INSTEAD OF");
else
appendStringInfo(&buf, "AFTER");
if (TRIGGER_FOR_INSERT(trigrec->tgtype))
*** ./src/backend/utils/adt/tsvector_op.c.orig 2010-08-15 00:18:37.000000000 +0100
--- ./src/backend/utils/adt/tsvector_op.c 2010-08-15 12:50:47.000000000 +0100
***************
*** 1259,1265 ****
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
--- 1259,1265 ----
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
*** ./src/bin/pg_dump/pg_dump.c.orig 2010-08-15 00:21:49.000000000 +0100
--- ./src/bin/pg_dump/pg_dump.c 2010-08-15 10:52:30.000000000 +0100
***************
*** 11745,11750 ****
--- 11745,11752 ----
findx = 0;
if (TRIGGER_FOR_BEFORE(tginfo->tgtype))
appendPQExpBuffer(query, "BEFORE");
+ if (TRIGGER_FOR_INSTEAD(tginfo->tgtype))
+ appendPQExpBuffer(query, "INSTEAD OF");
else
appendPQExpBuffer(query, "AFTER");
if (TRIGGER_FOR_INSERT(tginfo->tgtype))
*** ./src/bin/psql/describe.c.orig 2010-08-03 17:16:15.000000000 +0100
--- ./src/bin/psql/describe.c 2010-08-15 10:50:29.000000000 +0100
***************
*** 1820,1827 ****
--- 1820,1838 ----
}
PQclear(result);
}
+ }
+ /*
+ * Print triggers next.
+ * This is part of the footer information about a table, but may also
+ * apply to a view.
+ */
+ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v')
+ {
/* print triggers (but only user-defined triggers) */
+ PGresult *result = NULL;
+ int tuples = 0;
+
if (tableinfo.hastriggers)
{
printfPQExpBuffer(&buf,
***************
*** 1935,1942 ****
--- 1946,1963 ----
}
PQclear(result);
}
+ }
+ /*
+ * Done printing trigger information for view or table.
+ * Finish printing the footer information about a table.
+ */
+ if (tableinfo.relkind == 'r')
+ {
/* print inherited tables */
+ PGresult *result = NULL;
+ int tuples = 0;
+
printfPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i WHERE c.oid=i.inhparent AND i.inhrelid = '%s' ORDER BY inhseqno", oid);
result = PSQLexec(buf.data, false);
*** ./src/include/catalog/pg_trigger.h.orig 2010-07-31 10:05:35.000000000 +0100
--- ./src/include/catalog/pg_trigger.h 2010-08-15 09:50:50.000000000 +0100
***************
*** 91,96 ****
--- 91,97 ----
#define TRIGGER_TYPE_DELETE (1 << 3)
#define TRIGGER_TYPE_UPDATE (1 << 4)
#define TRIGGER_TYPE_TRUNCATE (1 << 5)
+ #define TRIGGER_TYPE_INSTEAD (1 << 6)
/* Macros for manipulating tgtype */
#define TRIGGER_CLEAR_TYPE(type) ((type) = 0)
***************
*** 101,106 ****
--- 102,108 ----
#define TRIGGER_SETT_DELETE(type) ((type) |= TRIGGER_TYPE_DELETE)
#define TRIGGER_SETT_UPDATE(type) ((type) |= TRIGGER_TYPE_UPDATE)
#define TRIGGER_SETT_TRUNCATE(type) ((type) |= TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_SETT_INSTEAD(type) ((type) |= TRIGGER_TYPE_INSTEAD)
#define TRIGGER_FOR_ROW(type) ((type) & TRIGGER_TYPE_ROW)
#define TRIGGER_FOR_BEFORE(type) ((type) & TRIGGER_TYPE_BEFORE)
***************
*** 108,112 ****
--- 110,115 ----
#define TRIGGER_FOR_DELETE(type) ((type) & TRIGGER_TYPE_DELETE)
#define TRIGGER_FOR_UPDATE(type) ((type) & TRIGGER_TYPE_UPDATE)
#define TRIGGER_FOR_TRUNCATE(type) ((type) & TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_FOR_INSTEAD(type) ((type) & TRIGGER_TYPE_INSTEAD)
#endif /* PG_TRIGGER_H */
*** ./src/include/commands/trigger.h.orig 2010-08-15 00:24:40.000000000 +0100
--- ./src/include/commands/trigger.h 2010-08-15 10:24:18.000000000 +0100
***************
*** 53,58 ****
--- 53,59 ----
#define TRIGGER_EVENT_OPMASK 0x00000003
#define TRIGGER_EVENT_ROW 0x00000004
#define TRIGGER_EVENT_BEFORE 0x00000008
+ #define TRIGGER_EVENT_INSTEAD 0x00000040
/* More TriggerEvent flags, used only within trigger.c */
***************
*** 84,91 ****
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event))
/*
* Definitions for the replication role based firing.
--- 85,95 ----
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
+ #define TRIGGER_FIRED_INSTEAD(event) \
+ ((TriggerEvent) (event) & TRIGGER_EVENT_INSTEAD)
+
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event) && !TRIGGER_FIRED_INSTEAD (event))
/*
* Definitions for the replication role based firing.
***************
*** 135,140 ****
--- 139,147 ----
ResultRelInfo *relinfo,
HeapTuple trigtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRInsertTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSDeleteTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASDeleteTriggers(EState *estate,
***************
*** 146,151 ****
--- 153,161 ----
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid);
+ extern HeapTuple ExecIRDeleteTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSUpdateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASUpdateTriggers(EState *estate,
***************
*** 160,165 ****
--- 170,179 ----
ItemPointer tupleid,
HeapTuple newtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRUpdateTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple oldtuple,
+ HeapTuple newtuple);
extern void ExecBSTruncateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASTruncateTriggers(EState *estate,
*** ./src/include/nodes/parsenodes.h.orig 2010-08-15 00:26:54.000000000 +0100
--- ./src/include/nodes/parsenodes.h 2010-08-15 09:45:52.000000000 +0100
***************
*** 1608,1613 ****
--- 1608,1614 ----
List *funcname; /* qual. name of function to call */
List *args; /* list of (T_String) Values or NIL */
bool before; /* BEFORE/AFTER */
+ bool instead; /* INSTEAD OF (overrides BEFORE/AFTER) */
bool row; /* ROW/STATEMENT */
/* events uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */
int16 events; /* INSERT/UPDATE/DELETE/TRUNCATE */
*** ./src/include/utils/rel.h.orig 2010-08-15 00:30:33.000000000 +0100
--- ./src/include/utils/rel.h 2010-08-15 10:20:43.000000000 +0100
***************
*** 82,91 ****
--- 82,93 ----
uint16 n_before_statement[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_before_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ uint16 n_instead_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ int *tg_instead_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_statement[TRIGGER_NUM_EVENT_CLASSES];
/* The actual array of triggers is here */
*** ./src/pl/plperl/plperl.c.orig 2010-07-31 12:19:30.000000000 +0100
--- ./src/pl/plperl/plperl.c 2010-08-15 12:44:58.000000000 +0100
***************
*** 954,959 ****
--- 954,961 ----
when = "BEFORE";
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
when = "AFTER";
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ when = "INSTEAD";
else
when = "UNKNOWN";
hv_store_string(hv, "when", newSVstring(when));
*** ./src/pl/plpgsql/src/pl_exec.c.orig 2010-08-15 00:34:14.000000000 +0100
--- ./src/pl/plpgsql/src/pl_exec.c 2010-08-15 12:44:01.000000000 +0100
***************
*** 581,588 ****
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE or AFTER");
var->isnull = false;
var->freeval = true;
--- 581,590 ----
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ var->value = CStringGetTextDatum("INSTEAD");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE, AFTER or INSTEAD");
var->isnull = false;
var->freeval = true;
*** ./src/pl/plpython/plpython.c.orig 2010-07-31 12:26:04.000000000 +0100
--- ./src/pl/plpython/plpython.c 2010-08-15 12:46:11.000000000 +0100
***************
*** 846,851 ****
--- 846,853 ----
pltwhen = PyString_FromString("BEFORE");
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
pltwhen = PyString_FromString("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ pltwhen = PyString_FromString("INSTEAD");
else
{
elog(ERROR, "unrecognized WHEN tg_event: %u", tdata->tg_event);
*** ./src/pl/tcl/pltcl.c.orig 2010-07-31 12:26:53.000000000 +0100
--- ./src/pl/tcl/pltcl.c 2010-08-15 12:47:27.000000000 +0100
***************
*** 822,827 ****
--- 822,829 ----
Tcl_DStringAppendElement(&tcl_cmd, "BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
Tcl_DStringAppendElement(&tcl_cmd, "AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ Tcl_DStringAppendElement(&tcl_cmd, "INSTEAD");
else
elog(ERROR, "unrecognized WHEN tg_event: %u", trigdata->tg_event);
*** ./src/test/regress/expected/triggers.out.orig 2010-08-15 15:55:21.000000000 +0100
--- ./src/test/regress/expected/triggers.out 2010-08-15 16:04:07.000000000 +0100
***************
*** 791,793 ****
--- 791,1191 ----
DROP TABLE min_updates_test;
DROP TABLE min_updates_test_oids;
+ --
+ -- Test triggers on views
+ --
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ ERROR: cannot insert into a view
+ HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+ UPDATE main_view SET b=20 WHERE a=50;
+ ERROR: cannot update a view
+ HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+ DELETE FROM main_view WHERE a=50;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a=51;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- Triggers that aren't allowed on views (these should all fail)
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ -- INSTEAD OF trigger function
+ CREATE OR REPLACE FUNCTION instead_of_trigger() 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 = 'INSERT' then
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ DELETE FROM main_table WHERE a=OLD.a AND b=OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end;
+ $$;
+ -- The following INSTEAD OF triggers shouldn't be allowed
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_ins');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_del');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+ ERROR: cannot use a WHEN condition with an INSTEAD OF trigger
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+ ERROR: cannot use a column list with an INSTEAD OF trigger
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+ ERROR: STATEMENT-level INSTEAD OF triggers are not supported
+ HINT: Use FOR EACH ROW for INSTEAD OF triggers
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_ins');
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_del');
+ -- Valid BEFORE and AFTER statement triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_view_ins_stmt');
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_view_ins_stmt');
+ \set QUIET false
+ INSERT INTO main_view VALUES (20, 30);
+ NOTICE: trigger_func(before_view_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ NOTICE: TG_NAME: instead_of_insert_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: INSERT
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_ins]
+ NOTICE: NEW: (20,30)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "instead_of_trigger" line 44 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "instead_of_trigger" line 44 at SQL statement
+ NOTICE: trigger_func(after_view_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ INSERT 0 1
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+ NOTICE: trigger_func(before_view_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ NOTICE: TG_NAME: instead_of_insert_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: INSERT
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_ins]
+ NOTICE: NEW: (21,31)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "instead_of_trigger" line 44 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "instead_of_trigger" line 44 at SQL statement
+ NOTICE: trigger_func(after_view_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ a | b
+ ----+----
+ 21 | 31
+ (1 row)
+
+ INSERT 0 1
+ UPDATE main_view SET b=31 WHERE a=20;
+ NOTICE: TG_NAME: instead_of_update_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: UPDATE
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_upd]
+ NOTICE: OLD: (20,30)
+ NOTICE: NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ UPDATE 0
+ UPDATE main_view SET b=32 WHERE a=21 AND b=31 RETURNING a,b;
+ NOTICE: TG_NAME: instead_of_update_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: UPDATE
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_upd]
+ NOTICE: OLD: (21,31)
+ NOTICE: NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ a | b
+ ---+---
+ (0 rows)
+
+ UPDATE 0
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ DROP TRIGGER
+ UPDATE main_view SET b=31 WHERE a=20;
+ NOTICE: TG_NAME: instead_of_update_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: UPDATE
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_upd]
+ NOTICE: OLD: (20,30)
+ NOTICE: NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ UPDATE 1
+ UPDATE main_view SET b=32 WHERE a=21 AND b=31 RETURNING a,b;
+ NOTICE: TG_NAME: instead_of_update_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: UPDATE
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_upd]
+ NOTICE: OLD: (21,31)
+ NOTICE: NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b"
+ PL/pgSQL function "instead_of_trigger" line 49 at SQL statement
+ a | b
+ ----+----
+ 21 | 32
+ (1 row)
+
+ UPDATE 1
+ DELETE FROM main_view WHERE a IN (20,21);
+ NOTICE: TG_NAME: instead_of_delete_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: DELETE
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_del]
+ NOTICE: OLD: (21,10)
+ NOTICE: TG_NAME: instead_of_delete_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: DELETE
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_del]
+ NOTICE: OLD: (20,31)
+ NOTICE: TG_NAME: instead_of_delete_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: DELETE
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_del]
+ NOTICE: OLD: (21,32)
+ DELETE 3
+ DELETE FROM main_view WHERE a=31 RETURNING a,b;
+ NOTICE: TG_NAME: instead_of_delete_trig
+ NOTICE: TG_WHEN: INSTEAD
+ NOTICE: TG_LEVEL: ROW
+ NOTICE: TG_OP: DELETE
+ NOTICE: TG_RELID::regclass: main_view
+ NOTICE: TG_RELNAME: main_view
+ NOTICE: TG_TABLE_NAME: main_view
+ NOTICE: TG_TABLE_SCHEMA: public
+ NOTICE: TG_NARGS: 1
+ NOTICE: TG_ARGV: [instead_of_del]
+ NOTICE: OLD: (31,10)
+ a | b
+ ----+----
+ 31 | 10
+ (1 row)
+
+ DELETE 1
+ \set QUIET true
+ \d main_view
+ View "public.main_view"
+ Column | Type | Modifiers
+ --------+---------+-----------
+ a | integer |
+ b | integer |
+ Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_view_ins_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_view_ins_stmt')
+ instead_of_delete_trig INSTEAD OF DELETE ON main_view FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_del')
+ instead_of_insert_trig INSTEAD OF INSERT ON main_view FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_ins')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_upd')
+
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d main_view
+ View "public.main_view"
+ Column | Type | Modifiers
+ --------+---------+-----------
+ a | integer |
+ b | integer |
+ Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_view_ins_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_view_ins_stmt')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_upd')
+
+ DROP VIEW main_view;
*** ./src/test/regress/regress.c.orig 2010-07-31 12:28:32.000000000 +0100
--- ./src/test/regress/regress.c 2010-08-15 12:52:20.000000000 +0100
***************
*** 489,495 ****
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
--- 489,495 ----
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
*** ./src/test/regress/sql/triggers.sql.orig 2010-08-15 13:15:16.000000000 +0100
--- ./src/test/regress/sql/triggers.sql 2010-08-15 16:03:34.000000000 +0100
***************
*** 579,581 ****
--- 579,740 ----
DROP TABLE min_updates_test_oids;
+ --
+ -- Test triggers on views
+ --
+
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ UPDATE main_view SET b=20 WHERE a=50;
+ DELETE FROM main_view WHERE a=50;
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a=51;
+
+ -- Triggers that aren't allowed on views (these should all fail)
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ -- INSTEAD OF trigger function
+ CREATE OR REPLACE FUNCTION instead_of_trigger() 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 = 'INSERT' then
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ UPDATE main_table SET a=NEW.a, b=NEW.b WHERE a=OLD.a AND b=OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ DELETE FROM main_table WHERE a=OLD.a AND b=OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end;
+ $$;
+
+ -- The following INSTEAD OF triggers shouldn't be allowed
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_ins');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_del');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_ins');
+
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_upd');
+
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE instead_of_trigger('instead_of_del');
+
+ -- Valid BEFORE and AFTER statement triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_view_ins_stmt');
+
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_view_ins_stmt');
+
+ \set QUIET false
+
+ INSERT INTO main_view VALUES (20, 30);
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+
+ UPDATE main_view SET b=31 WHERE a=20;
+ UPDATE main_view SET b=32 WHERE a=21 AND b=31 RETURNING a,b;
+
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ UPDATE main_view SET b=31 WHERE a=20;
+ UPDATE main_view SET b=32 WHERE a=21 AND b=31 RETURNING a,b;
+
+ DELETE FROM main_view WHERE a IN (20,21);
+ DELETE FROM main_view WHERE a=31 RETURNING a,b;
+
+ \set QUIET true
+
+ \d main_view
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d main_view
+ DROP VIEW main_view;
On 15 August 2010 18:38, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
There are still a number of things left todo:
- extend ALTER VIEW with enable/disable trigger commands
On further reflection, I wonder if the ability to disable VIEW
triggers is needed/wanted at all. I just noticed that while it is
possible to disable a RULE on a TABLE, it is not possible to do so on
VIEW. This certainly makes sense for the _RETURN rule, although
possibly some people might have a use for disabling other rules on
views. The situation with triggers is similar - disabling an INSTEAD
OF trigger would be pointless, and could only lead to errors when
updating the view. Some people may have a use case for disabling
BEFORE and AFTER statement triggers on views, but I suspect that the
number of such cases is small, and I'm tempted to omit this, for now
at least.
Thoughts?
- Dean
Dean Rasheed <dean.a.rasheed@gmail.com> writes:
On 15 August 2010 18:38, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
There are still a number of things left todo:
�- extend ALTER VIEW with enable/disable trigger commands
On further reflection, I wonder if the ability to disable VIEW
triggers is needed/wanted at all.
AFAIK the only real use for disabling triggers is in connection with
trigger-based replication systems. A view wouldn't be carrying
replication-related triggers anyway, so I think this could certainly
be left out of a first cut.
You aren't saying that you can see some reason why this couldn't be
added later, if wanted, correct?
regards, tom lane
On 16 August 2010 18:50, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Dean Rasheed <dean.a.rasheed@gmail.com> writes:
On 15 August 2010 18:38, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
There are still a number of things left todo:
- extend ALTER VIEW with enable/disable trigger commandsOn further reflection, I wonder if the ability to disable VIEW
triggers is needed/wanted at all.AFAIK the only real use for disabling triggers is in connection with
trigger-based replication systems. A view wouldn't be carrying
replication-related triggers anyway, so I think this could certainly
be left out of a first cut.You aren't saying that you can see some reason why this couldn't be
added later, if wanted, correct?
Yes. It should be easy to add later if wanted. I just don't see much
use for it, and I don't want to add more to an already quite big
patch, if it's not really needed.
Regards,
Dean
On 15 August 2010 18:38, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Here is a WIP patch implementing triggers on VIEWs ... <snip>
There are still a number of things left todo:
- extend ALTER VIEW with enable/disable trigger commands
- much more testing
- documentation
Attached is an updated patch with more tests and docs, and a few minor
code tidy ups. I think that the INSTEAD OF triggers part of the patch
is compliant with Feature T213 of the SQL 2008 standard. As discussed,
I don't plan to add the syntax to allow triggers on views to be
disabled at this time, but that should be easy to implement, if there
is a use case for it.
Comments welcome.
Regards,
Dean
Attachments:
view_triggers.patchapplication/octet-stream; name=view_triggers.patchDownload
*** ./contrib/spi/autoinc.c.orig 2010-07-31 12:06:47.000000000 +0100
--- ./contrib/spi/autoinc.c 2010-08-15 12:55:08.000000000 +0100
***************
*** 38,44 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
--- 38,44 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
*** ./contrib/spi/insert_username.c.orig 2010-07-31 12:07:44.000000000 +0100
--- ./contrib/spi/insert_username.c 2010-08-15 12:56:59.000000000 +0100
***************
*** 41,47 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
--- 41,47 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
*** ./contrib/spi/moddatetime.c.orig 2010-07-31 12:10:47.000000000 +0100
--- ./contrib/spi/moddatetime.c 2010-08-15 12:57:35.000000000 +0100
***************
*** 47,53 ****
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
--- 47,53 ----
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
*** ./contrib/spi/timetravel.c.orig 2010-07-31 12:13:01.000000000 +0100
--- ./contrib/spi/timetravel.c 2010-08-15 16:42:48.000000000 +0100
***************
*** 122,128 ****
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
--- 122,128 ----
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
*** ./doc/src/sgml/catalogs.sgml.orig 2010-09-04 14:38:30.000000000 +0100
--- ./doc/src/sgml/catalogs.sgml 2010-09-04 15:04:08.000000000 +0100
***************
*** 4711,4717 ****
</indexterm>
<para>
! The catalog <structname>pg_trigger</structname> stores triggers on tables.
See <xref linkend="sql-createtrigger">
for more information.
</para>
--- 4711,4717 ----
</indexterm>
<para>
! The catalog <structname>pg_trigger</structname> stores triggers on tables and views.
See <xref linkend="sql-createtrigger">
for more information.
</para>
*** ./doc/src/sgml/information_schema.sgml.orig 2010-08-31 11:57:59.000000000 +0100
--- ./doc/src/sgml/information_schema.sgml 2010-08-31 12:59:42.000000000 +0100
***************
*** 4885,4892 ****
<para>
The view <literal>triggers</literal> contains all triggers defined
! in the current database on tables that the current user owns or has
! some non-SELECT privilege on.
</para>
<table>
--- 4885,4892 ----
<para>
The view <literal>triggers</literal> contains all triggers defined
! in the current database on tables and views that the current user owns
! or has some non-SELECT privilege on.
</para>
<table>
***************
*** 4990,4997 ****
<entry><literal>condition_timing</literal></entry>
<entry><type>character_data</type></entry>
<entry>
! Time at which the trigger fires (<literal>BEFORE</literal> or
! <literal>AFTER</literal>)
</entry>
</row>
--- 4990,4997 ----
<entry><literal>condition_timing</literal></entry>
<entry><type>character_data</type></entry>
<entry>
! Time at which the trigger fires (<literal>BEFORE</literal>,
! <literal>AFTER</literal> or <literal>INSTEAD OF</literal>)
</entry>
</row>
***************
*** 5562,5580 ****
<row>
<entry><literal>is_trigger_updatable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>Applies to a feature not available in <productname>PostgreSQL</></entry>
</row>
<row>
<entry><literal>is_trigger_deletable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>Applies to a feature not available in <productname>PostgreSQL</></entry>
</row>
<row>
<entry><literal>is_trigger_insertable_into</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>Applies to a feature not available in <productname>PostgreSQL</></entry>
</row>
</tbody>
</tgroup>
--- 5562,5589 ----
<row>
<entry><literal>is_trigger_updatable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>
! <literal>YES</> if the view has an <literal>INSTEAD OF</>
! <command>UPDATE</> trigger defined on it, <literal>NO</> if not
! </entry>
</row>
<row>
<entry><literal>is_trigger_deletable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>
! <literal>YES</> if the view has an <literal>INSTEAD OF</>
! <command>DELETE</> trigger defined on it, <literal>NO</> if not
! </entry>
</row>
<row>
<entry><literal>is_trigger_insertable_into</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>
! <literal>YES</> if the view has an <literal>INSTEAD OF</>
! <command>INSERT</> trigger defined on it, <literal>NO</> if not
! </entry>
</row>
</tbody>
</tgroup>
*** ./doc/src/sgml/plperl.sgml.orig 2010-09-01 15:41:47.000000000 +0100
--- ./doc/src/sgml/plperl.sgml 2010-09-01 16:51:11.000000000 +0100
***************
*** 980,986 ****
<term><literal>$_TD->{when}</literal></term>
<listitem>
<para>
! When the trigger was called: <literal>BEFORE</literal>, <literal>AFTER</literal>, or <literal>UNKNOWN</literal>
</para>
</listitem>
</varlistentry>
--- 980,988 ----
<term><literal>$_TD->{when}</literal></term>
<listitem>
<para>
! When the trigger was called: <literal>BEFORE</literal>,
! <literal>AFTER</literal>, <literal>INSTEAD OF</literal> or
! <literal>UNKNOWN</literal>
</para>
</listitem>
</varlistentry>
*** ./doc/src/sgml/plpgsql.sgml.orig 2010-08-30 20:13:35.000000000 +0100
--- ./doc/src/sgml/plpgsql.sgml 2010-09-04 19:39:00.000000000 +0100
***************
*** 3112,3120 ****
<term><varname>TG_WHEN</varname></term>
<listitem>
<para>
! Data type <type>text</type>; a string of either
! <literal>BEFORE</literal> or <literal>AFTER</literal>
! depending on the trigger's definition.
</para>
</listitem>
</varlistentry>
--- 3112,3120 ----
<term><varname>TG_WHEN</varname></term>
<listitem>
<para>
! Data type <type>text</type>; a string of
! <literal>BEFORE</literal>, <literal>AFTER</literal> or
! <literal>INSTEAD OF</literal> depending on the trigger's definition.
</para>
</listitem>
</varlistentry>
***************
*** 3239,3244 ****
--- 3239,3259 ----
</para>
<para>
+ <literal>INSTEAD OF</> triggers (which are always row-level triggers,
+ and may only be used on views) can return null to signal that they did
+ not perform any updates, and that the rest of the operation for this
+ row should be skipped (i.e., subsequent triggers are not fired, and the
+ <command>INSERT</>/<command>UPDATE</>/<command>DELETE</> did not occur
+ for this row). Otherwise a nonnull value should be returned, to signal
+ that the trigger performed the requested operation. For
+ <command>INSERT</> and <command>UPDATE</> operations, the return value
+ should be <varname>NEW</>, which the trigger function may modify to
+ support <command>INSERT RETURNING</> and <command>UPDATE RETURNING</>.
+ For <command>DELETE</> operations, the return value should be
+ <varname>OLD</>.
+ </para>
+
+ <para>
The return value of a row-level trigger
fired <literal>AFTER</literal> or a statement-level trigger
fired <literal>BEFORE</> or <literal>AFTER</> is
***************
*** 3354,3359 ****
--- 3369,3453 ----
</programlisting>
</example>
+ <para>
+ A variation of the previous example uses a view joining the main table
+ to the audit table, to show when each entry was last modified. This
+ approach still records the full audit trail of changes to the table,
+ but also presents a simplified view of the audit trail, showing just
+ the last modified timestamp derived from the audit trail for each entry.
+ <xref linkend="plpgsql-view-trigger-audit-example"> shows an example
+ of an audit trigger on a view in <application>PL/pgSQL</application>.
+ </para>
+
+ <example id="plpgsql-view-trigger-audit-example">
+ <title>A <application>PL/pgSQL</application> View Trigger Procedure For Auditing</title>
+
+ <para>
+ This example uses a trigger on the view to make it updatable, and
+ ensure that any insert, update or delete of a row in the view is
+ recorded (i.e., audited) in the emp_audit table. The current time
+ and user name are recorded, together with the type of operation
+ performed, and the view displays the last modified time of each row.
+ </para>
+
+ <programlisting>
+ CREATE TABLE emp (
+ empname text PRIMARY KEY,
+ salary integer
+ );
+
+ CREATE TABLE emp_audit(
+ operation char(1) NOT NULL,
+ userid text NOT NULL,
+ empname text NOT NULL,
+ salary integer,
+ stamp timestamp NOT NULL
+ );
+
+ CREATE VIEW emp_view AS
+ SELECT e.empname,
+ e.salary,
+ max(ea.stamp) AS last_updated
+ FROM emp e
+ LEFT JOIN emp_audit ea ON ea.empname = e.empname
+ GROUP BY 1, 2;
+
+ CREATE OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$
+ BEGIN
+ --
+ -- Perform the required operation on emp, and create a row in emp_audit
+ -- to reflect the change made to emp.
+ --
+ IF (TG_OP = 'DELETE') THEN
+ DELETE FROM emp WHERE empname = OLD.empname;
+ IF NOT FOUND THEN RETURN NULL; END IF;
+
+ OLD.last_updated = now();
+ INSERT INTO emp_audit VALUES('D', user, OLD.*);
+ RETURN OLD;
+ ELSIF (TG_OP = 'UPDATE') THEN
+ UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;
+ IF NOT FOUND THEN RETURN NULL; END IF;
+
+ NEW.last_updated = now();
+ INSERT INTO emp_audit VALUES('U', user, NEW.*);
+ RETURN NEW;
+ ELSIF (TG_OP = 'INSERT') THEN
+ INSERT INTO emp VALUES(NEW.empname, NEW.salary);
+
+ NEW.last_updated = now();
+ INSERT INTO emp_audit VALUES('I', user, NEW.*);
+ RETURN NEW;
+ END IF;
+ END;
+ $$ LANGUAGE plpgsql;
+
+ CREATE TRIGGER emp_audit
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
+ FOR EACH ROW EXECUTE PROCEDURE update_emp_view();
+ </programlisting>
+ </example>
+
<para>
One use of triggers is to maintain a summary table
of another table. The resulting summary can be used in place of the
*** ./doc/src/sgml/plpython.sgml.orig 2010-09-01 18:01:23.000000000 +0100
--- ./doc/src/sgml/plpython.sgml 2010-09-01 18:14:17.000000000 +0100
***************
*** 751,757 ****
<listitem>
<para>
contains one of <literal>BEFORE</>, <literal>AFTER</>,
! or <literal>UNKNOWN</>.
</para>
</listitem>
</varlistentry>
--- 751,757 ----
<listitem>
<para>
contains one of <literal>BEFORE</>, <literal>AFTER</>,
! <literal>INSTEAD OF</> or <literal>UNKNOWN</>.
</para>
</listitem>
</varlistentry>
***************
*** 827,838 ****
</para>
<para>
! If <literal>TD["when"]</literal> is <literal>BEFORE</> and
<literal>TD["level"]</literal> is <literal>ROW</>, you can
return <literal>None</literal> or <literal>"OK"</literal> from the
Python function to indicate the row is unmodified,
! <literal>"SKIP"</> to abort the event, or <literal>"MODIFY"</> to
! indicate you've modified the row.
Otherwise the return value is ignored.
</para>
</sect1>
--- 827,840 ----
</para>
<para>
! If <literal>TD["when"]</literal> is <literal>BEFORE</> or
! <literal>INSTEAD OF</> and
<literal>TD["level"]</literal> is <literal>ROW</>, you can
return <literal>None</literal> or <literal>"OK"</literal> from the
Python function to indicate the row is unmodified,
! <literal>"SKIP"</> to abort the event, or if <literal>TD["event"]</>
! is <command>INSERT</> or <command>UPDATE</> you can return
! <literal>"MODIFY"</> to indicate you've modified the new row.
Otherwise the return value is ignored.
</para>
</sect1>
*** ./doc/src/sgml/pltcl.sgml.orig 2010-09-01 15:07:44.000000000 +0100
--- ./doc/src/sgml/pltcl.sgml 2010-09-01 17:48:37.000000000 +0100
***************
*** 567,574 ****
<term><varname>$TG_when</varname></term>
<listitem>
<para>
! The string <literal>BEFORE</> or <literal>AFTER</> depending on the
! type of trigger event.
</para>
</listitem>
</varlistentry>
--- 567,574 ----
<term><varname>$TG_when</varname></term>
<listitem>
<para>
! The string <literal>BEFORE</>, <literal>AFTER</> or
! <literal>INSTEAD OF</> depending on the type of trigger event.
</para>
</listitem>
</varlistentry>
***************
*** 641,650 ****
the operation (<command>INSERT</>/<command>UPDATE</>/<command>DELETE</>) that fired the trigger will proceed
normally. <literal>SKIP</> tells the trigger manager to silently suppress
the operation for this row. If a list is returned, it tells PL/Tcl to
! return a modified row to the trigger manager that will be inserted
! instead of the one given in <varname>$NEW</>. (This works for <command>INSERT</> and <command>UPDATE</>
! only.) Needless to say that all this is only meaningful when the trigger
! is <literal>BEFORE</> and <command>FOR EACH ROW</>; otherwise the return value is ignored.
</para>
<para>
--- 641,654 ----
the operation (<command>INSERT</>/<command>UPDATE</>/<command>DELETE</>) that fired the trigger will proceed
normally. <literal>SKIP</> tells the trigger manager to silently suppress
the operation for this row. If a list is returned, it tells PL/Tcl to
! return a modified row to the trigger manager. This is only meaningful
! for row-level <literal>BEFORE</> <command>INSERT</> or <command>UPDATE</>
! triggers for which the modified row will be inserted instead of the one
! given in <varname>$NEW</>; or for row-level <literal>INSTEAD OF</>
! <command>INSERT</> or <command>UPDATE</> triggers where the returned row
! is used to support <command>INSERT RETURNING</> and
! <command>UPDATE RETURNING</> commands. The return value is ignored for
! other types of triggers.
</para>
<para>
*** ./doc/src/sgml/ref/create_rule.sgml.orig 2010-09-01 18:44:58.000000000 +0100
--- ./doc/src/sgml/ref/create_rule.sgml 2010-09-01 18:55:21.000000000 +0100
***************
*** 73,79 ****
sufficient for your purposes) to replace update actions on the view
with appropriate updates on other tables. If you want to support
<command>INSERT RETURNING</> and so on, then be sure to put a suitable
! <literal>RETURNING</> clause into each of these rules.
</para>
<para>
--- 73,81 ----
sufficient for your purposes) to replace update actions on the view
with appropriate updates on other tables. If you want to support
<command>INSERT RETURNING</> and so on, then be sure to put a suitable
! <literal>RETURNING</> clause into each of these rules. Alternatively,
! an updatable view may be implemented using <literal>INSTEAD OF</>
! triggers (see <xref linkend="sql-createtrigger">).
</para>
<para>
*** ./doc/src/sgml/ref/create_trigger.sgml.orig 2010-09-01 19:04:12.000000000 +0100
--- ./doc/src/sgml/ref/create_trigger.sgml 2010-09-04 18:14:52.000000000 +0100
***************
*** 21,27 ****
<refsynopsisdiv>
<synopsis>
! CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTER } { <replaceable class="PARAMETER">event</replaceable> [ OR ... ] }
ON <replaceable class="PARAMETER">table</replaceable> [ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( <replaceable class="parameter">condition</replaceable> ) ]
EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
--- 21,27 ----
<refsynopsisdiv>
<synopsis>
! CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTER | INSTEAD OF } { <replaceable class="PARAMETER">event</replaceable> [ OR ... ] }
ON <replaceable class="PARAMETER">table</replaceable> [ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( <replaceable class="parameter">condition</replaceable> ) ]
EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
***************
*** 33,53 ****
<para>
<command>CREATE TRIGGER</command> creates a new trigger. The
! trigger will be associated with the specified table and will
execute the specified function <replaceable
class="parameter">function_name</replaceable> when certain events occur.
</para>
<para>
! The trigger can be specified to fire either before the
operation is attempted on a row (before constraints are checked and
the <command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> is attempted) or after the operation has
completed (after constraints are checked and the
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> has completed). If the trigger fires
! before the event, the trigger can skip the operation for the
! current row, or change the row being inserted (for
<command>INSERT</command> and <command>UPDATE</command> operations
only). If the trigger fires after the event, all changes, including
the effects of other triggers, are <quote>visible</quote>
--- 33,54 ----
<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.
</para>
<para>
! The trigger can be specified to fire before the
operation is attempted on a row (before constraints are checked and
the <command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> is attempted); after the operation has
completed (after constraints are checked and the
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> has completed); or instead of the operation
! (in the case of inserts, updates or deletes on a view).
! If the trigger fires before or instead of the event, the trigger can skip
! the operation for the current row, or change the row being inserted (for
<command>INSERT</command> and <command>UPDATE</command> operations
only). If the trigger fires after the event, all changes, including
the effects of other triggers, are <quote>visible</quote>
***************
*** 68,86 ****
</para>
<para>
! In addition, triggers may be defined to fire for a
! <command>TRUNCATE</command>, though only
! <literal>FOR EACH STATEMENT</literal>.
</para>
<para>
! Also, a trigger definition can specify a Boolean <literal>WHEN</>
condition, which will be tested to see whether the trigger should
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
is not so useful for them since the condition cannot refer to any values
! in the table.
</para>
<para>
--- 69,144 ----
</para>
<para>
! Triggers that are specified to fire <literal>INSTEAD OF</> the trigger
! event must be marked <literal>FOR EACH ROW</>, and can only be defined
! on views. <literal>BEFORE</> and <literal>AFTER</> triggers on a view
! must be marked as <literal>FOR EACH STATEMENT</>.
</para>
<para>
! In addition, triggers may be defined to fire for a
! <command>TRUNCATE</command> (tables only), though only
! <literal>FOR EACH STATEMENT</literal>. The following table
! summarises which types of triggers may be used on tables and views:
! </para>
!
! <informaltable id="supported-trigger-types">
! <tgroup cols="4">
! <thead>
! <row>
! <entry>When</entry>
! <entry>Event</entry>
! <entry>Row-level</entry>
! <entry>Statement-level</entry>
! </row>
! </thead>
! <tbody>
! <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>
! </row>
! <row>
! <entry align="center"><command>TRUNCATE</></entry>
! <entry align="center">X</entry>
! <entry align="center">Tables</entry>
! </row>
! <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>
! </row>
! <row>
! <entry align="center"><command>TRUNCATE</></entry>
! <entry align="center">X</entry>
! <entry align="center">Tables</entry>
! </row>
! <row>
! <entry align="center" morerows="1"><literal>INSTEAD OF</></entry>
! <entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
! <entry align="center">Views</entry>
! <entry align="center">X</entry>
! </row>
! <row>
! <entry align="center"><command>TRUNCATE</></entry>
! <entry align="center">X</entry>
! <entry align="center">X</entry>
! </row>
! </tbody>
! </tgroup>
! </informaltable>
!
! <para>
! Also, most trigger definitions can specify a Boolean <literal>WHEN</>
condition, which will be tested to see whether the trigger should
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
is not so useful for them since the condition cannot refer to any values
! in the table. <literal>INSTEAD OF</> triggers cannot be used with
! <literal>WHEN</> conditions.
</para>
<para>
***************
*** 116,125 ****
<varlistentry>
<term><literal>BEFORE</literal></term>
<term><literal>AFTER</literal></term>
<listitem>
<para>
! Determines whether the function is called before or after the
! event.
</para>
</listitem>
</varlistentry>
--- 174,184 ----
<varlistentry>
<term><literal>BEFORE</literal></term>
<term><literal>AFTER</literal></term>
+ <term><literal>INSTEAD OF</literal></term>
<listitem>
<para>
! Determines whether the function is called before, after or instead of
! the event.
</para>
</listitem>
</varlistentry>
***************
*** 143,148 ****
--- 202,213 ----
The trigger will only fire if at least one of the listed columns
is mentioned as a target of the update.
</para>
+
+ <para>
+ <literal>INSTEAD OF</> triggers do not support lists of columns. An
+ <literal>INSTEAD OF</> trigger must be unconditional, and is fired
+ for each affected row in the view.
+ </para>
</listitem>
</varlistentry>
***************
*** 150,156 ****
<term><replaceable class="parameter">table</replaceable></term>
<listitem>
<para>
! The name (optionally schema-qualified) of the table the trigger
is for.
</para>
</listitem>
--- 215,221 ----
<term><replaceable class="parameter">table</replaceable></term>
<listitem>
<para>
! The name (optionally schema-qualified) of the table or view the trigger
is for.
</para>
</listitem>
***************
*** 189,194 ****
--- 254,264 ----
</para>
<para>
+ <literal>INSTEAD OF</> triggers do not support <literal>WHEN</>
+ conditions.
+ </para>
+
+ <para>
Currently, <literal>WHEN</literal> expressions cannot contain
subqueries.
</para>
***************
*** 326,331 ****
--- 396,411 ----
WHEN (OLD.* IS DISTINCT FROM NEW.*)
EXECUTE PROCEDURE log_account_update();
</programlisting>
+
+ Execute the function <function>view_insert_row</> for each row to insert
+ rows into a view:
+
+ <programlisting>
+ CREATE TRIGGER view_insert
+ INSTEAD OF INSERT ON my_view
+ FOR EACH ROW
+ EXECUTE PROCEDURE view_insert_row();
+ </programlisting>
</para>
<para>
***************
*** 396,402 ****
<para>
The ability to fire triggers for <command>TRUNCATE</command> is a
! <productname>PostgreSQL</> extension of the SQL standard.
</para>
</refsect1>
--- 476,485 ----
<para>
The ability to fire triggers for <command>TRUNCATE</command> is a
! <productname>PostgreSQL</> extension of the SQL standard, as is the
! ability to define statement-level triggers on views that fire
! <literal>BEFORE</> or <literal>AFTER</> <command>INSERT</>,
! <command>UPDATE</> and <command>DELETE</> commands.
</para>
</refsect1>
*** ./doc/src/sgml/rules.sgml.orig 2010-08-28 18:12:42.000000000 +0100
--- ./doc/src/sgml/rules.sgml 2010-08-31 14:02:45.000000000 +0100
***************
*** 168,176 ****
<para>
<command>DELETE</command> commands don't need a target list
because they don't produce any result. In fact, the planner will
! add a special <acronym>CTID</> entry to the empty target list, but
! this is after the rule system and will be discussed later; for the
! rule system, the target list is empty.
</para>
<para>
--- 168,178 ----
<para>
<command>DELETE</command> commands don't need a target list
because they don't produce any result. In fact, the planner will
! add a special <acronym>CTID</> entry to the empty target list (if the
! result relation is a table), but this is after the rule system and
! will be discussed later; for the rule system, the target list is empty.
! If the result relation is a view, the rule system will behave slightly
! differently, as described later.
</para>
<para>
***************
*** 189,198 ****
For <command>UPDATE</command> commands, the target list
describes the new rows that should replace the old ones. In the
rule system, it contains just the expressions from the <literal>SET
! column = expression</literal> part of the command. The planner will handle
! missing columns by inserting expressions that copy the values from
! the old row into the new one. And it will add the special
! <acronym>CTID</> entry just as for <command>DELETE</command>, too.
</para>
<para>
--- 191,202 ----
For <command>UPDATE</command> commands, the target list
describes the new rows that should replace the old ones. In the
rule system, it contains just the expressions from the <literal>SET
! column = expression</literal> part of the command. The planner will
! handle missing columns by inserting expressions that copy the values
! from the old row into the new one. And it will add the special
! <acronym>CTID</> entry, if the result relation is a table, just as
! for <command>DELETE</command>. If the result relation is a view,
! the rule system will behave slightly differently, as described later.
</para>
<para>
***************
*** 656,662 ****
<para>
Two details of the query tree aren't touched in the description of
view rules above. These are the command type and the result relation.
! In fact, view rules don't need this information.
</para>
<para>
--- 660,668 ----
<para>
Two details of the query tree aren't touched in the description of
view rules above. These are the command type and the result relation.
! In fact, the command type is not needed by view rules, but the result
! relation may affect the way in which the query rewriter works, because
! special care needs to be taken if the result relation is a view.
</para>
<para>
***************
*** 725,745 ****
</programlisting>
will do. But there is a little problem in
! <command>UPDATE</command>: The executor does not care what the
! results from the join it is doing are meant for. It just produces
! a result set of rows. The difference that one is a
! <command>SELECT</command> command and the other is an
! <command>UPDATE</command> is handled in the caller of the
! executor. The caller still knows (looking at the query tree) that
! this is an <command>UPDATE</command>, and it knows that this
! result should go into table <literal>t1</>. But which of the rows that are
! there has to be replaced by the new row?
</para>
<para>
! To resolve this problem, another entry is added to the target list
! in <command>UPDATE</command> (and also in
! <command>DELETE</command>) statements: the current tuple ID
(<acronym>CTID</>).<indexterm><primary>CTID</></>
This is a system column containing the
file block number and position in the block for the row. Knowing
--- 731,750 ----
</programlisting>
will do. But there is a little problem in
! <command>UPDATE</command>: The branch of the executor plan that does
! the join does not care what the results from the join it is doing are
! meant for. It just produces a result set of rows. The difference that
! one is a <command>SELECT</command> command and the other is an
! <command>UPDATE</command> is handled higher up in the executor, where
! it knows that this is an <command>UPDATE</command>, and it knows that
! this result should go into table <literal>t1</>. But which of the rows
! that are there has to be replaced by the new row?
</para>
<para>
! If the result relation is a table, this problem is resolved by adding
! another entry to the target list in <command>UPDATE</command> (and
! also in <command>DELETE</command>) statements: the current tuple ID
(<acronym>CTID</>).<indexterm><primary>CTID</></>
This is a system column containing the
file block number and position in the block for the row. Knowing
***************
*** 764,770 ****
<para>
Knowing all that, we can simply apply view rules in absolutely
! the same way to any command. There is no difference.
</para>
</sect2>
--- 769,777 ----
<para>
Knowing all that, we can simply apply view rules in absolutely
! the same way to any command. There is no difference, unless the view is
! the result relation, which must be handled differently (see
! <xref linkend="rules-views-update">).
</para>
</sect2>
***************
*** 803,818 ****
<para>
What happens if a view is named as the target relation for an
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command>? After doing the substitutions
! described above, we will have a query tree in which the result
! relation points at a subquery range-table entry. This will not
! work, so the rewriter throws an error if it sees it has produced
! such a thing.
</para>
<para>
! To change this, we can define rules that modify the behavior of
! these kinds of commands. This is the topic of the next section.
</para>
</sect2>
--- 810,859 ----
<para>
What happens if a view is named as the target relation for an
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command>? Simply doing the substitutions
! described above would give a query tree in which the result
! relation points at a subquery range-table entry, which would not
! work. The rewriter handles this case slightly differently, leaving
! the original view range-table entry as the result relation, and for
! <command>UPDATE</command> and <command>DELETE</command> operations,
! adding a new subquery range-table entry based on the view's query.
</para>
<para>
! Another problem that now arises is how to identify the rows to be
! updated in the result relation. Recall that when the result relation
! was a table, a special <acronym>CTID</> entry was added to the target
! list to identify the physical locations of the rows to be updated.
! This does not work if the result relation is a view, because a view
! typically does not have a <acronym>CTID</>, and its rows may not have
! unique physical locations. Instead, for an <command>UPDATE</command>
! or <command>DELETE</command> operation, a special <literal>wholerow</>
! entry is added to the target list, which expands to include all
! columns from the view. The executor does not know how to update the
! view; it relies on <literal>INSTEAD OF</> triggers for this. The
! executor uses the <literal>wholerow</> target list entry to pass the
! data from all the columns in the view to the triggers, to allow them
! to work out what to update.
! </para>
!
! <para>
! If there are no <literal>INSTEAD OF</> triggers to update the view,
! the executor will throw an error, because it cannot automatically
! update a view by itself. To change this, we can define rules that
! modify the behavior of <command>INSERT</command>,
! <command>UPDATE</command>, and <command>DELETE</command> commands on
! a view. These rules will rewrite the command, typically into a command
! that updates one or more tables, rather than views. This is the topic
! of the next section.
! </para>
!
! <para>
! Note that rules are evaluated first, rewriting the original query
! before it is planned and executed. Therefore, if a view has
! <literal>INSTEAD OF</> triggers as well as rules on <command>INSERT</>,
! <command>UPDATE</>, and <command>DELETE</>, then the rules will be
! evaluated first, and depending on the result, the triggers may not be
! used at all.
</para>
</sect2>
***************
*** 1868,1876 ****
</para>
<para>
! On the other hand, a trigger cannot be created on views because
! there is no real data in a view relation; however INSERT, UPDATE,
! and DELETE rules can be created on views.
</para>
<para>
--- 1909,1923 ----
</para>
<para>
! In this chapter, we focused on using rules to update views. All of
! the update rule examples in this chapter can also be implemented
! using <literal>INSTEAD OF</> triggers on the views (triggers defined
! to fire instead of the view update operations, with trigger functions
! that do the <command>INSERT</>, <command>UPDATE</> and
! <command>DELETE</> operations on the base tables). Writing such
! triggers is often easier than writing rules, particularly if complex
! logic is required to perform the update, however, it is possible that
! for bulk update operations, rules may perform better than triggers.
</para>
<para>
*** ./doc/src/sgml/trigger.sgml.orig 2010-08-28 18:09:13.000000000 +0100
--- ./doc/src/sgml/trigger.sgml 2010-09-01 17:17:41.000000000 +0100
***************
*** 33,39 ****
<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 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.
--- 33,43 ----
<para>
A trigger is a specification that the database should automatically
execute a particular function whenever a certain type of operation is
! performed.
! </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,
or once per <acronym>SQL</acronym> statement.
***************
*** 46,51 ****
--- 50,68 ----
</para>
<para>
+ On views, triggers can be defined to execute instead of
+ <command>INSERT</command>, <command>UPDATE</command>, or
+ <command>DELETE</command> operations, and are fired once per row that
+ needs to be modified in the view. It is the responsibility of the
+ trigger's function to perform the necessary modifications to the
+ underlying base tables and, where appropriate, return the modified
+ row as it appears in the view. Triggers can also be defined to
+ execute once per <acronym>SQL</acronym> statement, before or after
+ <command>INSERT</command>, <command>UPDATE</command>, or
+ <command>DELETE</command> operations.
+ </para>
+
+ <para>
The trigger function must be defined before the trigger itself can be
created. The trigger function must be declared as a
function taking no arguments and returning type <literal>trigger</>.
***************
*** 74,91 ****
two types of triggers are sometimes called <firstterm>row-level</>
triggers and <firstterm>statement-level</> triggers,
respectively. Triggers on <command>TRUNCATE</command> may only be
! defined at statement-level.
</para>
<para>
! Triggers are also classified as <firstterm>before</> triggers and
! <firstterm>after</> triggers.
Statement-level before triggers naturally fire before the
statement starts to do anything, while statement-level after
! triggers fire at the very end of the statement. Row-level before
triggers fire immediately before a particular row is operated on,
while row-level after triggers fire at the end of the statement
! (but before any statement-level after triggers).
</para>
<para>
--- 91,115 ----
two types of triggers are sometimes called <firstterm>row-level</>
triggers and <firstterm>statement-level</> triggers,
respectively. Triggers on <command>TRUNCATE</command> may only be
! defined at statement-level. On views, triggers that fire before or
! after may only be defined at statement-level, and triggers that fire
! instead of an <command>INSERT</command>, <command>UPDATE</command>,
! or <command>DELETE</command> may only be defined at row-level.
</para>
<para>
! Triggers are also classified as <firstterm>before</> triggers,
! <firstterm>after</> triggers or <firstterm>instead of</> triggers.
Statement-level before triggers naturally fire before the
statement starts to do anything, while statement-level after
! triggers fire at the very end of the statement. These types of
! triggers may be defined on tables or views. Row-level before
triggers fire immediately before a particular row is operated on,
while row-level after triggers fire at the end of the statement
! (but before any statement-level after triggers). These types of
! triggers may only be defined on tables. Row-level 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>
***************
*** 126,131 ****
--- 150,171 ----
</para>
<para>
+ A row-level instead of trigger should either return
+ <symbol>NULL</> to indicate that it did not modify any data from
+ the view's underlying base tables, or it should return the view
+ row that was passed in (the <varname>NEW</varname> row
+ for <command>INSERT</command> and <command>UPDATE</command>
+ operations, or the <varname>OLD</varname> row for
+ <command>DELETE</command> operations). A nonnull return value is
+ used to signal that the trigger performed the necessary data
+ modifications in the view, and will cause the count of the number
+ of rows affected in the command status to be incremented. For
+ <command>INSERT</> and <command>UPDATE</> operations, the trigger
+ may modify the <varname>NEW</> row before returning it, to support
+ <command>INSERT RETURNING</> and <command>UPDATE RETURNING</>.
+ </para>
+
+ <para>
The return value is ignored for row-level triggers fired after an
operation, and so they can return <symbol>NULL</>.
</para>
***************
*** 133,141 ****
<para>
If more than one trigger is defined for the same event on the same
relation, the triggers will be fired in alphabetical order by
! trigger name. In the case of before triggers, the
possibly-modified row returned by each trigger becomes the input
! to the next trigger. If any before trigger returns
<symbol>NULL</>, the operation is abandoned for that row and subsequent
triggers are not fired.
</para>
--- 173,181 ----
<para>
If more than one trigger is defined for the same event on the same
relation, the triggers will be fired in alphabetical order by
! trigger name. In the case of before and instead of triggers, the
possibly-modified row returned by each trigger becomes the input
! to the next trigger. If any before or instead of trigger returns
<symbol>NULL</>, the operation is abandoned for that row and subsequent
triggers are not fired.
</para>
***************
*** 157,162 ****
--- 197,203 ----
to queue an event nor to re-fetch the row at end of statement. This
can result in significant speedups in statements that modify many
rows, if the trigger only needs to be fired for a few of the rows.
+ Instead of triggers on views do not support <literal>WHEN</> conditions.
</para>
<para>
***************
*** 376,381 ****
--- 417,431 ----
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>TRIGGER_FIRED_INSTEAD(tg_event)</literal></term>
+ <listitem>
+ <para>
+ Returns true if the trigger fired instead of the operation.
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><literal>TRIGGER_FIRED_AFTER(tg_event)</literal></term>
*** ./src/backend/catalog/index.c.orig 2010-08-14 23:52:07.000000000 +0100
--- ./src/backend/catalog/index.c 2010-08-15 09:53:21.000000000 +0100
***************
*** 825,830 ****
--- 825,831 ----
trigger->funcname = SystemFuncName("unique_key_recheck");
trigger->args = NIL;
trigger->before = false;
+ trigger->instead = false;
trigger->row = true;
trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE;
trigger->columns = NIL;
*** ./src/backend/catalog/information_schema.sql.orig 2010-08-28 16:44:27.000000000 +0100
--- ./src/backend/catalog/information_schema.sql 2010-08-31 12:43:19.000000000 +0100
***************
*** 1937,1943 ****
CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END
AS character_data) AS action_orientation,
CAST(
! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' ELSE 'AFTER' END
AS character_data) AS condition_timing,
CAST(null AS sql_identifier) AS condition_reference_old_table,
CAST(null AS sql_identifier) AS condition_reference_new_table,
--- 1937,1943 ----
CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END
AS character_data) AS action_orientation,
CAST(
! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' WHEN t.tgtype & 64 = 64 THEN 'INSTEAD OF' ELSE 'AFTER' END
AS character_data) AS condition_timing,
CAST(null AS sql_identifier) AS condition_reference_old_table,
CAST(null AS sql_identifier) AS condition_reference_new_table,
***************
*** 2233,2241 ****
THEN 'YES' ELSE 'NO' END
AS yes_or_no) AS is_insertable_into,
! CAST('NO' AS yes_or_no) AS is_trigger_updatable,
! CAST('NO' AS yes_or_no) AS is_trigger_deletable,
! CAST('NO' AS yes_or_no) AS is_trigger_insertable_into
FROM pg_namespace nc, pg_class c
--- 2233,2252 ----
THEN 'YES' ELSE 'NO' END
AS yes_or_no) AS is_insertable_into,
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 80 = 80)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_updatable,
!
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 72 = 72)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_deletable,
!
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 68 = 68)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_insertable_into
FROM pg_namespace nc, pg_class c
*** ./src/backend/catalog/sql_features.txt.orig 2010-09-04 15:10:12.000000000 +0100
--- ./src/backend/catalog/sql_features.txt 2010-09-04 15:10:42.000000000 +0100
***************
*** 426,432 ****
T211 Basic trigger capability 07 TRIGGER privilege YES
T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted
T212 Enhanced trigger capability YES
! T213 INSTEAD OF triggers NO
T231 Sensitive cursors YES
T241 START TRANSACTION statement YES
T251 SET TRANSACTION statement: LOCAL option NO
--- 426,432 ----
T211 Basic trigger capability 07 TRIGGER privilege YES
T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted
T212 Enhanced trigger capability YES
! T213 INSTEAD OF triggers YES
T231 Sensitive cursors YES
T241 START TRANSACTION statement YES
T251 SET TRANSACTION statement: LOCAL option NO
*** ./src/backend/commands/tablecmds.c.orig 2010-08-19 08:50:53.000000000 +0100
--- ./src/backend/commands/tablecmds.c 2010-08-19 08:50:52.000000000 +0100
***************
*** 5619,5624 ****
--- 5619,5625 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = myRel;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
/* Either ON INSERT or ON UPDATE */
***************
*** 5682,5687 ****
--- 5683,5689 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_DELETE;
fk_trigger->columns = NIL;
***************
*** 5735,5740 ****
--- 5737,5743 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_UPDATE;
fk_trigger->columns = NIL;
*** ./src/backend/commands/trigger.c.orig 2010-08-28 17:15:25.000000000 +0100
--- ./src/backend/commands/trigger.c 2010-09-01 16:58:12.000000000 +0100
***************
*** 150,159 ****
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 150,198 ----
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind == RELKIND_RELATION)
! {
! /* Forbid INSTEAD OF triggers on tables */
! if (stmt->instead)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a table",
! RelationGetRelationName(rel)),
! errhint("Define INSTEAD OF triggers on views")));
! }
! else if (rel->rd_rel->relkind == RELKIND_VIEW)
! {
! /* Only allow INSTEAD OF or STATEMENT-level triggers on views */
! if (!stmt->instead && stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a view",
! RelationGetRelationName(rel)),
! errhint("Define ROW-level BEFORE and AFTER triggers on tables")));
!
! /* Disallow STATEMENT-level INSTEAD OF triggers */
! if (stmt->instead && !stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("STATEMENT-level INSTEAD OF triggers are not supported"),
! errhint("Use FOR EACH ROW for INSTEAD OF triggers")));
!
! /* Disallow WHEN clauses with INSTEAD OF triggers */
! if (stmt->instead && stmt->whenClause)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a WHEN condition with an INSTEAD OF trigger")));
!
! /* Disallow column selection with INSTEAD OF triggers */
! if (stmt->instead && stmt->columns != NIL)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a column list with an INSTEAD OF trigger")));
! }
! else
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
***************
*** 188,193 ****
--- 227,234 ----
TRIGGER_CLEAR_TYPE(tgtype);
if (stmt->before)
TRIGGER_SETT_BEFORE(tgtype);
+ if (stmt->instead)
+ TRIGGER_SETT_INSTEAD(tgtype);
if (stmt->row)
TRIGGER_SETT_ROW(tgtype);
tgtype |= stmt->events;
***************
*** 198,203 ****
--- 239,252 ----
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("TRUNCATE FOR EACH ROW triggers are not supported")));
+ /* Disallow TRUNCATE triggers on VIEWs */
+ if (rel->rd_rel->relkind == RELKIND_VIEW && TRIGGER_FOR_TRUNCATE(tgtype))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is a view",
+ RelationGetRelationName(rel)),
+ errhint("Define TRUNCATE triggers on tables")));
+
/*
* Parse the WHEN clause, if any
*/
***************
*** 1031,1040 ****
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 1080,1090 ----
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION &&
! rel->rd_rel->relkind != RELKIND_VIEW)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
***************
*** 1506,1511 ****
--- 1556,1566 ----
n = trigdesc->n_before_row;
t = trigdesc->tg_before_row;
}
+ else if (TRIGGER_FOR_INSTEAD(trigger->tgtype))
+ {
+ n = trigdesc->n_instead_row;
+ t = trigdesc->tg_instead_row;
+ }
else
{
n = trigdesc->n_after_row;
***************
*** 1656,1661 ****
--- 1711,1729 ----
else
t[i] = NULL;
}
+ n = newdesc->n_instead_row;
+ t = newdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ {
+ if (n[i] > 0)
+ {
+ tnew = (int *) palloc(n[i] * sizeof(int));
+ memcpy(tnew, t[i], n[i] * sizeof(int));
+ t[i] = tnew;
+ }
+ else
+ t[i] = NULL;
+ }
n = newdesc->n_after_statement;
t = newdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
***************
*** 1698,1703 ****
--- 1766,1775 ----
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
pfree(t[i]);
+ t = trigdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ if (t[i] != NULL)
+ pfree(t[i]);
t = trigdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
***************
*** 2000,2005 ****
--- 2072,2121 ----
true, NULL, trigtuple, recheckIndexes, NULL);
}
+ HeapTuple
+ ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_INSERT];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_INSERT];
+ TriggerData LocTriggerData;
+ HeapTuple newtuple = trigtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, NULL, trigtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != trigtuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
{
***************
*** 2134,2139 ****
--- 2250,2297 ----
}
}
+ bool
+ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_DELETE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_DELETE];
+ TriggerData LocTriggerData;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, trigtuple, NULL))
+ continue;
+
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple == NULL)
+ return false; /* Delete was suppressed */
+ if (rettuple != trigtuple)
+ heap_freetuple(rettuple);
+ }
+ return true;
+ }
+
void
ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
{
***************
*** 2281,2286 ****
--- 2439,2488 ----
}
}
+ HeapTuple
+ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple oldtuple, HeapTuple newtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_UPDATE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_UPDATE];
+ TriggerData LocTriggerData;
+ HeapTuple intuple = newtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, oldtuple, newtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = oldtuple;
+ LocTriggerData.tg_newtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != intuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
{
*** ./src/backend/executor/execMain.c.orig 2010-08-15 00:04:31.000000000 +0100
--- ./src/backend/executor/execMain.c 2010-08-15 10:01:37.000000000 +0100
***************
*** 899,908 ****
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("cannot change view \"%s\"",
! RelationGetRelationName(resultRelationDesc))));
break;
default:
ereport(ERROR,
--- 899,905 ----
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! /* OK */
break;
default:
ereport(ERROR,
*** ./src/backend/executor/nodeModifyTable.c.orig 2010-08-19 08:53:10.000000000 +0100
--- ./src/backend/executor/nodeModifyTable.c 2010-09-04 22:53:39.000000000 +0100
***************
*** 149,154 ****
--- 149,200 ----
}
/* ----------------------------------------------------------------
+ * ExecCheckViewTriggers
+ *
+ * If the result relation is a view, check that it has the
+ * appropriate INSTEAD OF triggers to carry out the current
+ * operation.
+ * ----------------------------------------------------------------
+ */
+ static void
+ ExecCheckViewTriggers(ResultRelInfo *resultRelInfo,
+ CmdType operation)
+ {
+ TriggerDesc *trigDesc = resultRelInfo->ri_TrigDesc;
+
+ switch (operation)
+ {
+ case CMD_INSERT:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_INSERT] == 0)
+ ereport(ERROR,
+ (errmsg("cannot insert into a view"),
+ errhint("You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.")));
+ break;
+
+ case CMD_UPDATE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_UPDATE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot update a view"),
+ errhint("You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.")));
+ break;
+
+ case CMD_DELETE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_DELETE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot delete from a view"),
+ errhint("You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.")));
+ break;
+
+ default:
+ elog(ERROR, "unknown operation");
+ break;
+ }
+ }
+
+ /* ----------------------------------------------------------------
* ExecInsert
*
* For INSERT, we have to insert the tuple into the target relation
***************
*** 167,172 ****
--- 213,219 ----
Relation resultRelationDesc;
Oid newId;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* get the heap tuple out of the tuple table slot, making sure we have a
***************
*** 179,184 ****
--- 226,232 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
/*
* If the result relation has OIDs, force the tuple's OID to zero so that
***************
*** 195,202 ****
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /* BEFORE ROW INSERT Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
--- 243,251 ----
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /* BEFORE ROW INSERT Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
***************
*** 237,244 ****
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
(estate->es_processed)++;
estate->es_lastoid = newId;
--- 286,329 ----
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual insert
! */
! HeapTuple newtuple;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_INSERT);
! newtuple = ExecIRInsertTriggers(estate, resultRelInfo, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. 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(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! newId = InvalidOid;
! }
! else
! {
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
! }
(estate->es_processed)++;
estate->es_lastoid = newId;
***************
*** 251,258 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW INSERT Triggers */
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
--- 336,344 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW INSERT Triggers (tables only) */
! if (!resultRelIsView)
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
***************
*** 268,280 ****
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
--- 354,372 ----
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed.
! *
! * When deleting from a table, tupleid identifies the tuple to
! * 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.
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
***************
*** 284,298 ****
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW DELETE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
--- 376,393 ----
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
+ bool resultRelIsView;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /* BEFORE ROW DELETE Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
***************
*** 313,323 ****
* referential integrity updates in serializable transactions.
*/
ldelete:;
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 408,443 ----
* referential integrity updates in serializable transactions.
*/
ldelete:;
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual delete
! */
! HeapTupleData tuple;
! bool diddelete;
!
! tuple.t_data = oldtuple;
! tuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(tuple.t_self));
! tuple.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_DELETE);
! diddelete = ExecIRDeleteTriggers(estate, resultRelInfo, &tuple);
!
! if (!diddelete) /* trigger(s) did nothing */
! return NULL;
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
***************
*** 367,374 ****
* anyway, since the tuple is still visible to other transactions.
*/
! /* AFTER ROW DELETE Triggers */
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 487,495 ----
* anyway, since the tuple is still visible to other transactions.
*/
! /* AFTER ROW DELETE Triggers (tables only) */
! if (!resultRelIsView)
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
***************
*** 382,391 ****
HeapTupleData deltuple;
Buffer delbuffer;
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
--- 503,522 ----
HeapTupleData deltuple;
Buffer delbuffer;
! if (resultRelIsView)
! {
! deltuple.t_data = oldtuple;
! deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(deltuple.t_self));
! deltuple.t_tableOid = InvalidOid;
! }
! else
! {
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
! }
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
***************
*** 395,401 ****
slot, planSlot);
ExecClearTuple(slot);
! ReleaseBuffer(delbuffer);
return rslot;
}
--- 526,533 ----
slot, planSlot);
ExecClearTuple(slot);
! if (!resultRelIsView)
! ReleaseBuffer(delbuffer);
return rslot;
}
***************
*** 413,423 ****
--- 545,561 ----
* is, we don't want to get stuck in an infinite loop
* which corrupts your database..
*
+ * When updating a table, tupleid identifies the tuple to
+ * 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.
+ *
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecUpdate(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EPQState *epqstate,
***************
*** 430,435 ****
--- 568,574 ----
ItemPointerData update_ctid;
TransactionId update_xmax;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* abort the operation if not running transactions
***************
*** 448,456 ****
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW UPDATE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
--- 587,597 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /* BEFORE ROW UPDATE Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
***************
*** 501,511 ****
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in serializable transactions.
*/
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 642,696 ----
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in serializable transactions.
*/
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual update
! */
! HeapTupleData oldtup;
! HeapTuple newtuple;
!
! oldtup.t_data = oldtuple;
! oldtup.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(oldtup.t_self));
! oldtup.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_UPDATE);
! newtuple = ExecIRUpdateTriggers(estate, resultRelInfo,
! &oldtup, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. 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(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
***************
*** 568,576 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW UPDATE Triggers */
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
--- 753,762 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW UPDATE Triggers (tables only) */
! if (!resultRelIsView)
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
***************
*** 654,659 ****
--- 840,846 ----
TupleTableSlot *planSlot;
ItemPointer tupleid = NULL;
ItemPointerData tuple_ctid;
+ HeapTupleHeader oldtuple;
/*
* On first call, fire BEFORE STATEMENT triggers before proceeding.
***************
*** 713,735 ****
if (junkfilter != NULL)
{
/*
! * extract the 'ctid' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! /* shouldn't ever get a null result... */
! if (isNull)
! elog(ERROR, "ctid is NULL");
!
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
}
/*
--- 900,936 ----
if (junkfilter != NULL)
{
+ ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
+ Relation resultRel = resultRelInfo->ri_RelationDesc;
+
/*
! * extract the 'ctid' or 'wholerow' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "wholerow is NULL");
!
! oldtuple = DatumGetHeapTupleHeader(datum);
! }
! else
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "ctid is NULL");
!
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
! }
}
/*
***************
*** 745,755 ****
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, planSlot,
&node->mt_epqstate, estate);
break;
default:
--- 946,956 ----
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, oldtuple, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, oldtuple, planSlot,
&node->mt_epqstate, estate);
break;
default:
***************
*** 833,838 ****
--- 1034,1043 ----
i = 0;
foreach(l, node->plans)
{
+ Relation resultRel = estate->es_result_relation_info->ri_RelationDesc;
+ if (resultRel->rd_rel->relkind == RELKIND_VIEW)
+ ExecCheckViewTriggers(estate->es_result_relation_info, operation);
+
subplan = (Plan *) lfirst(l);
mtstate->mt_plans[i] = ExecInitNode(subplan, estate, eflags);
estate->es_result_relation_info++;
***************
*** 988,997 ****
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /* For UPDATE/DELETE, find the ctid junk attr now */
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
}
resultRelInfo->ri_junkFilter = j;
--- 1193,1216 ----
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /*
! * For UPDATE/DELETE of real tables, find the ctid junk
! * attr now. For VIEWs use the wholerow junk attr.
! */
! Relation resultRel = resultRelInfo->ri_RelationDesc;
!
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk wholerow column");
! }
! else
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
! }
}
resultRelInfo->ri_junkFilter = j;
*** ./src/backend/nodes/copyfuncs.c.orig 2010-08-19 08:55:28.000000000 +0100
--- ./src/backend/nodes/copyfuncs.c 2010-08-19 08:55:27.000000000 +0100
***************
*** 3229,3234 ****
--- 3229,3235 ----
COPY_NODE_FIELD(funcname);
COPY_NODE_FIELD(args);
COPY_SCALAR_FIELD(before);
+ COPY_SCALAR_FIELD(instead);
COPY_SCALAR_FIELD(row);
COPY_SCALAR_FIELD(events);
COPY_NODE_FIELD(columns);
*** ./src/backend/nodes/equalfuncs.c.orig 2010-08-19 08:56:51.000000000 +0100
--- ./src/backend/nodes/equalfuncs.c 2010-08-19 08:56:50.000000000 +0100
***************
*** 1686,1691 ****
--- 1686,1692 ----
COMPARE_NODE_FIELD(funcname);
COMPARE_NODE_FIELD(args);
COMPARE_SCALAR_FIELD(before);
+ COMPARE_SCALAR_FIELD(instead);
COMPARE_SCALAR_FIELD(row);
COMPARE_SCALAR_FIELD(events);
COMPARE_NODE_FIELD(columns);
*** ./src/backend/optimizer/plan/planmain.c.orig 2010-07-28 09:37:12.000000000 +0100
--- ./src/backend/optimizer/plan/planmain.c 2010-08-15 09:29:08.000000000 +0100
***************
*** 180,185 ****
--- 180,194 ----
add_base_rels_to_query(root, (Node *) parse->jointree);
/*
+ * If the query target is a VIEW, it won't be in the jointree, but we
+ * need a dummy RelOptInfo node for it. This need not have any stats in
+ * it because it always just goes at the top of the plan tree.
+ */
+ if (parse->resultRelation &&
+ root->simple_rel_array[parse->resultRelation] == NULL)
+ build_simple_rel(root, parse->resultRelation, RELOPT_OTHER_MEMBER_REL);
+
+ /*
* Examine the targetlist and qualifications, adding entries to baserel
* targetlists for all referenced Vars. Restrict and join clauses are
* added to appropriate lists belonging to the mentioned relations. We
*** ./src/backend/optimizer/prep/preptlist.c.orig 2010-07-28 09:37:12.000000000 +0100
--- ./src/backend/optimizer/prep/preptlist.c 2010-08-15 09:35:45.000000000 +0100
***************
*** 38,44 ****
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table);
/*
--- 38,44 ----
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel);
/*
***************
*** 52,57 ****
--- 52,58 ----
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
+ bool result_is_view = false;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
***************
*** 63,81 ****
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
}
/*
! * for heap_form_tuple to work, the targetlist must match the exact order
! * of the attributes. We also need to fill in any missing attributes. -ay
! * 10/94
*/
! if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
! tlist = expand_targetlist(tlist, command_type,
! result_relation, range_table);
/*
* for "update" and "delete" queries, add ctid of the result relation into
--- 64,100 ----
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
+ Relation rel;
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
+
+ /*
+ * Open the result relation. We assume that the rewriter already
+ * acquired at least AccessShareLock on it, so we need no lock here.
+ */
+ rel = heap_open(getrelid(result_relation, range_table), NoLock);
+ result_is_view = rel->rd_rel->relkind == RELKIND_VIEW;
+
+ /*
+ * for heap_form_tuple to work, the targetlist must match the exact
+ * order of the attributes. We also need to fill in any missing
+ * attributes. -ay 10/94
+ */
+ if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ tlist = expand_targetlist(tlist, command_type,
+ result_relation, rel);
+
+ heap_close(rel, NoLock);
}
/*
! * For an UPDATE, expand_targetlist already created a fresh tlist. For
! * DELETE, better do a listCopy so that we don't destructively modify
! * the original tlist (is this really necessary?).
*/
! if (command_type == CMD_DELETE)
! tlist = list_copy(tlist);
/*
* for "update" and "delete" queries, add ctid of the result relation into
***************
*** 83,90 ****
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
*/
! if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
{
TargetEntry *tle;
Var *var;
--- 102,114 ----
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
+ *
+ * We don't do this if the result relation is a view, since that won't
+ * expose a ctid. The rewriter should have already added a wholerow TLE
+ * for the view's subselect.
*/
! if (!result_is_view &&
! (command_type == CMD_UPDATE || command_type == CMD_DELETE))
{
TargetEntry *tle;
Var *var;
***************
*** 97,110 ****
pstrdup("ctid"),
true);
- /*
- * For an UPDATE, expand_targetlist already created a fresh tlist. For
- * DELETE, better do a listCopy so that we don't destructively modify
- * the original tlist (is this really necessary?).
- */
- if (command_type == CMD_DELETE)
- tlist = list_copy(tlist);
-
tlist = lappend(tlist, tle);
}
--- 121,126 ----
***************
*** 169,187 ****
}
else
{
! /* Not a table, so we need the whole row as a junk var */
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
}
}
--- 185,227 ----
}
else
{
! bool exists = false;
! ListCell *l;
!
! /*
! * Not a table, so we need the whole row as a junk var. If the
! * query target is a view, the rewriter will have already added
! * a whole row var, so don't add another for that RTE.
! */
! foreach(l, tlist)
! {
! TargetEntry *tle = (TargetEntry *) lfirst(l);
!
! if (tle->resjunk &&
! IsA(tle->expr, Var) &&
! ((Var *) tle->expr)->varno == rc->rti &&
! strcmp(tle->resname, "wholerow") == 0)
! {
! exists = true;
! break;
! }
! }
!
! if (!exists)
! {
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
! }
}
}
***************
*** 241,251 ****
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
- Relation rel;
int attrno,
numattrs;
--- 281,290 ----
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel)
{
List *new_tlist = NIL;
ListCell *tlist_item;
int attrno,
numattrs;
***************
*** 256,267 ****
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order. We assume
! * that the rewriter already acquired at least AccessShareLock on the
! * relation, so we need no lock here.
*/
- rel = heap_open(getrelid(result_relation, range_table), NoLock);
-
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
--- 295,302 ----
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order.
*/
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
***************
*** 399,406 ****
tlist_item = lnext(tlist_item);
}
- heap_close(rel, NoLock);
-
return new_tlist;
}
--- 434,439 ----
*** ./src/backend/optimizer/util/plancat.c.orig 2010-08-28 17:18:36.000000000 +0100
--- ./src/backend/optimizer/util/plancat.c 2010-08-28 17:18:35.000000000 +0100
***************
*** 441,447 ****
*tuples = 1;
break;
default:
! /* else it has no disk storage; probably shouldn't get here? */
*pages = 0;
*tuples = 0;
break;
--- 441,452 ----
*tuples = 1;
break;
default:
! /*
! * Else it has no disk storage; probably shouldn't get here,
! * unless this is a VIEW which is the query target, in which
! * case we don't care about the sizes, since it will always be
! * at the top of the plan tree.
! */
*pages = 0;
*tuples = 0;
break;
*** ./src/backend/parser/gram.y.orig 2010-08-28 17:26:46.000000000 +0100
--- ./src/backend/parser/gram.y 2010-08-28 17:26:44.000000000 +0100
***************
*** 245,251 ****
%type <str> OptSchemaName
%type <list> OptSchemaEltList
! %type <boolean> TriggerActionTime TriggerForSpec opt_trusted opt_restart_seqs
%type <list> TriggerEvents TriggerOneEvent
%type <value> TriggerFuncArg
--- 245,252 ----
%type <str> OptSchemaName
%type <list> OptSchemaEltList
! %type <boolean> TriggerForSpec opt_trusted opt_restart_seqs
! %type <ival> TriggerActionTime
%type <list> TriggerEvents TriggerOneEvent
%type <value> TriggerFuncArg
***************
*** 3377,3383 ****
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = $4;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
--- 3378,3385 ----
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = ($4 & 1) != 0;
! n->instead = ($4 & 2) != 0;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
***************
*** 3399,3404 ****
--- 3401,3407 ----
n->funcname = $17;
n->args = $19;
n->before = FALSE;
+ n->instead = FALSE;
n->row = TRUE;
n->events = intVal(linitial($6));
n->columns = (List *) lsecond($6);
***************
*** 3412,3419 ****
;
TriggerActionTime:
! BEFORE { $$ = TRUE; }
! | AFTER { $$ = FALSE; }
;
TriggerEvents:
--- 3415,3423 ----
;
TriggerActionTime:
! BEFORE { $$ = 1; }
! | AFTER { $$ = 0; }
! | INSTEAD OF { $$ = 2; }
;
TriggerEvents:
*** ./src/backend/rewrite/rewriteHandler.c.orig 2010-07-28 09:37:27.000000000 +0100
--- ./src/backend/rewrite/rewriteHandler.c 2010-08-15 09:07:20.000000000 +0100
***************
*** 564,570 ****
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
--- 564,572 ----
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions. For an UPDATE on a VIEW, add tlist entries assigning any
! * unassigned attributes their current values. The RHS of such assignments
! * will be rewritten to refer to the view's subselect node.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
***************
*** 724,729 ****
--- 726,753 ----
false);
}
+ /*
+ * For an UPDATE on a VIEW where the TLE is missing, assign the
+ * current value, which will be rewritten as a query from the view's
+ * subselect node by ApplyRetrieveRule().
+ */
+ if (new_tle == NULL && commandType == CMD_UPDATE &&
+ target_relation->rd_rel->relkind == RELKIND_VIEW)
+ {
+ Node *new_expr;
+
+ new_expr = (Node *) makeVar(parsetree->resultRelation,
+ attrno,
+ att_tup->atttypid,
+ att_tup->atttypmod,
+ 0);
+
+ new_tle = makeTargetEntry((Expr *) new_expr,
+ attrno,
+ pstrdup(NameStr(att_tup->attname)),
+ false);
+ }
+
if (new_tle)
new_tlist = lappend(new_tlist, new_tle);
}
***************
*** 1149,1154 ****
--- 1173,1180 ----
RangeTblEntry *rte,
*subrte;
RowMarkClause *rc;
+ Var *var;
+ TargetEntry *tle;
if (list_length(rule->actions) != 1)
elog(ERROR, "expected just one rule action");
***************
*** 1177,1220 ****
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! /*
! * VIEWs are really easy --- just plug the view query in as a subselect,
! * replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! /*
! * We move the view's permission check data down to its rangetable. The
! * checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
!
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as implicit
! * FOR UPDATE/SHARE, the same as the parser would have done if the view's
! * subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will be
! * made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
return parsetree;
}
--- 1203,1313 ----
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! if (rt_index == parsetree->resultRelation)
! {
! /*
! * We have a VIEW as the query target. For DELETE and UPDATE, we
! * add a new subselect RTE using the view's query and adjust any
! * VARs in the original query to point to this instead of the
! * original view RTE.
! *
! * We keep the original view in the rtable as the query target, and
! * any Vars in the returning list that reference it are left alone.
! *
! * The resulting jointree fromlist will not refer to the view RTE,
! * and so the planner won't try to join to it. This will result in
! * a plan with a ModifyTable node at the root, referring to the
! * original view relation, and a subselect based on the view's query
! * merged with any user conditions.
! *
! * For INSERTS we do nothing. The original view remains the query
! * target.
! */
! if (parsetree->commandType == CMD_DELETE ||
! parsetree->commandType == CMD_UPDATE)
! {
! List *returningList = parsetree->returningList;
! /*
! * Make a new subselect RTE from the view query and adjust the
! * the original query to point to this instead of the original
! * view, while preserving the view resultRelation and any
! * returningList Vars.
! */
! rte = copyObject(rt_fetch(rt_index, parsetree->rtable));
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! parsetree->rtable = lappend(parsetree->rtable, rte);
! parsetree->returningList = NIL;
!
! ChangeVarNodes((Node *) parsetree, rt_index,
! list_length(parsetree->rtable), 0);
!
! parsetree->resultRelation = rt_index;
! parsetree->returningList = returningList;
!
! /*
! * Add a "wholerow" junk TLE so that the executor can retrieve
! * the old VIEW tuples to pass to the INSTEAD OF triggers.
! */
! var = makeVar(list_length(parsetree->rtable),
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
!
! tle = makeTargetEntry((Expr *) var,
! list_length(parsetree->targetList) + 1,
! pstrdup("wholerow"),
! true);
! parsetree->targetList = lappend(parsetree->targetList, tle);
! }
! }
! else
! {
! /*
! * Selecting from the VIEW --- just plug the view query in as a
! * subselect, replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
!
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
!
! /*
! * We move the view's permission check data down to its rangetable.
! * The checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
!
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
!
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as
! * implicit FOR UPDATE/SHARE, the same as the parser would have done
! * if the view's subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will
! * be made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
! }
return parsetree;
}
*** ./src/backend/utils/adt/ruleutils.c.orig 2010-08-15 00:14:30.000000000 +0100
--- ./src/backend/utils/adt/ruleutils.c 2010-08-15 10:45:04.000000000 +0100
***************
*** 545,550 ****
--- 545,552 ----
if (TRIGGER_FOR_BEFORE(trigrec->tgtype))
appendStringInfo(&buf, "BEFORE");
+ else if (TRIGGER_FOR_INSTEAD(trigrec->tgtype))
+ appendStringInfo(&buf, "INSTEAD OF");
else
appendStringInfo(&buf, "AFTER");
if (TRIGGER_FOR_INSERT(trigrec->tgtype))
*** ./src/backend/utils/adt/tsvector_op.c.orig 2010-08-15 00:18:37.000000000 +0100
--- ./src/backend/utils/adt/tsvector_op.c 2010-08-15 12:50:47.000000000 +0100
***************
*** 1259,1265 ****
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
--- 1259,1265 ----
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
*** ./src/bin/pg_dump/pg_dump.c.orig 2010-08-15 00:21:49.000000000 +0100
--- ./src/bin/pg_dump/pg_dump.c 2010-08-15 10:52:30.000000000 +0100
***************
*** 11745,11750 ****
--- 11745,11752 ----
findx = 0;
if (TRIGGER_FOR_BEFORE(tginfo->tgtype))
appendPQExpBuffer(query, "BEFORE");
+ if (TRIGGER_FOR_INSTEAD(tginfo->tgtype))
+ appendPQExpBuffer(query, "INSTEAD OF");
else
appendPQExpBuffer(query, "AFTER");
if (TRIGGER_FOR_INSERT(tginfo->tgtype))
*** ./src/bin/psql/describe.c.orig 2010-08-03 17:16:15.000000000 +0100
--- ./src/bin/psql/describe.c 2010-08-15 10:50:29.000000000 +0100
***************
*** 1820,1827 ****
--- 1820,1838 ----
}
PQclear(result);
}
+ }
+ /*
+ * Print triggers next.
+ * This is part of the footer information about a table, but may also
+ * apply to a view.
+ */
+ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v')
+ {
/* print triggers (but only user-defined triggers) */
+ PGresult *result = NULL;
+ int tuples = 0;
+
if (tableinfo.hastriggers)
{
printfPQExpBuffer(&buf,
***************
*** 1935,1942 ****
--- 1946,1963 ----
}
PQclear(result);
}
+ }
+ /*
+ * Done printing trigger information for view or table.
+ * Finish printing the footer information about a table.
+ */
+ if (tableinfo.relkind == 'r')
+ {
/* print inherited tables */
+ PGresult *result = NULL;
+ int tuples = 0;
+
printfPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i WHERE c.oid=i.inhparent AND i.inhrelid = '%s' ORDER BY inhseqno", oid);
result = PSQLexec(buf.data, false);
*** ./src/include/catalog/pg_trigger.h.orig 2010-09-04 15:00:15.000000000 +0100
--- ./src/include/catalog/pg_trigger.h 2010-09-04 15:16:04.000000000 +0100
***************
*** 91,96 ****
--- 91,97 ----
#define TRIGGER_TYPE_DELETE (1 << 3)
#define TRIGGER_TYPE_UPDATE (1 << 4)
#define TRIGGER_TYPE_TRUNCATE (1 << 5)
+ #define TRIGGER_TYPE_INSTEAD (1 << 6)
/* Macros for manipulating tgtype */
#define TRIGGER_CLEAR_TYPE(type) ((type) = 0)
***************
*** 101,106 ****
--- 102,108 ----
#define TRIGGER_SETT_DELETE(type) ((type) |= TRIGGER_TYPE_DELETE)
#define TRIGGER_SETT_UPDATE(type) ((type) |= TRIGGER_TYPE_UPDATE)
#define TRIGGER_SETT_TRUNCATE(type) ((type) |= TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_SETT_INSTEAD(type) ((type) |= TRIGGER_TYPE_INSTEAD)
#define TRIGGER_FOR_ROW(type) ((type) & TRIGGER_TYPE_ROW)
#define TRIGGER_FOR_BEFORE(type) ((type) & TRIGGER_TYPE_BEFORE)
***************
*** 108,112 ****
--- 110,115 ----
#define TRIGGER_FOR_DELETE(type) ((type) & TRIGGER_TYPE_DELETE)
#define TRIGGER_FOR_UPDATE(type) ((type) & TRIGGER_TYPE_UPDATE)
#define TRIGGER_FOR_TRUNCATE(type) ((type) & TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_FOR_INSTEAD(type) ((type) & TRIGGER_TYPE_INSTEAD)
#endif /* PG_TRIGGER_H */
*** ./src/include/commands/trigger.h.orig 2010-08-15 00:24:40.000000000 +0100
--- ./src/include/commands/trigger.h 2010-09-01 16:59:34.000000000 +0100
***************
*** 53,58 ****
--- 53,59 ----
#define TRIGGER_EVENT_OPMASK 0x00000003
#define TRIGGER_EVENT_ROW 0x00000004
#define TRIGGER_EVENT_BEFORE 0x00000008
+ #define TRIGGER_EVENT_INSTEAD 0x00000040
/* More TriggerEvent flags, used only within trigger.c */
***************
*** 84,91 ****
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event))
/*
* Definitions for the replication role based firing.
--- 85,95 ----
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
+ #define TRIGGER_FIRED_INSTEAD(event) \
+ ((TriggerEvent) (event) & TRIGGER_EVENT_INSTEAD)
+
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event) && !TRIGGER_FIRED_INSTEAD (event))
/*
* Definitions for the replication role based firing.
***************
*** 135,140 ****
--- 139,147 ----
ResultRelInfo *relinfo,
HeapTuple trigtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRInsertTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSDeleteTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASDeleteTriggers(EState *estate,
***************
*** 146,151 ****
--- 153,161 ----
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid);
+ extern bool ExecIRDeleteTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSUpdateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASUpdateTriggers(EState *estate,
***************
*** 160,165 ****
--- 170,179 ----
ItemPointer tupleid,
HeapTuple newtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRUpdateTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple oldtuple,
+ HeapTuple newtuple);
extern void ExecBSTruncateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASTruncateTriggers(EState *estate,
*** ./src/include/nodes/parsenodes.h.orig 2010-08-19 08:59:46.000000000 +0100
--- ./src/include/nodes/parsenodes.h 2010-08-19 08:59:45.000000000 +0100
***************
*** 1608,1613 ****
--- 1608,1614 ----
List *funcname; /* qual. name of function to call */
List *args; /* list of (T_String) Values or NIL */
bool before; /* BEFORE/AFTER */
+ bool instead; /* INSTEAD OF (overrides BEFORE/AFTER) */
bool row; /* ROW/STATEMENT */
/* events uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */
int16 events; /* INSERT/UPDATE/DELETE/TRUNCATE */
*** ./src/include/utils/rel.h.orig 2010-08-15 00:30:33.000000000 +0100
--- ./src/include/utils/rel.h 2010-08-15 10:20:43.000000000 +0100
***************
*** 82,91 ****
--- 82,93 ----
uint16 n_before_statement[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_before_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ uint16 n_instead_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ int *tg_instead_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_statement[TRIGGER_NUM_EVENT_CLASSES];
/* The actual array of triggers is here */
*** ./src/pl/plperl/expected/plperl_trigger.out.orig 2010-09-04 14:58:26.000000000 +0100
--- ./src/pl/plperl/expected/plperl_trigger.out 2010-09-04 14:57:10.000000000 +0100
***************
*** 124,129 ****
--- 124,207 ----
CONTEXT: PL/Perl function "trigger_data"
DROP TRIGGER show_trigger_data_trig on trigger_test;
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'INSERT'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{new} = {'i' => '2', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ update trigger_test_view set v = 'update' where i = 1;
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'UPDATE'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{new} = {'i' => '1', 'v' => 'update'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ delete from trigger_test_view;
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'DELETE'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
DROP FUNCTION trigger_data();
CREATE OR REPLACE FUNCTION valid_id() RETURNS trigger AS $$
*** ./src/pl/plperl/plperl.c.orig 2010-07-31 12:19:30.000000000 +0100
--- ./src/pl/plperl/plperl.c 2010-08-30 23:39:05.000000000 +0100
***************
*** 954,959 ****
--- 954,961 ----
when = "BEFORE";
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
when = "AFTER";
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ when = "INSTEAD OF";
else
when = "UNKNOWN";
hv_store_string(hv, "when", newSVstring(when));
*** ./src/pl/plperl/sql/plperl_trigger.sql.orig 2010-09-04 10:57:46.000000000 +0100
--- ./src/pl/plperl/sql/plperl_trigger.sql 2010-09-04 11:13:40.000000000 +0100
***************
*** 60,65 ****
--- 60,79 ----
delete from trigger_test;
DROP TRIGGER show_trigger_data_trig on trigger_test;
+
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i = 1;
+ delete from trigger_test_view;
+
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
DROP FUNCTION trigger_data();
*** ./src/pl/plpgsql/src/pl_exec.c.orig 2010-08-28 17:29:02.000000000 +0100
--- ./src/pl/plpgsql/src/pl_exec.c 2010-08-30 23:37:49.000000000 +0100
***************
*** 581,588 ****
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE or AFTER");
var->isnull = false;
var->freeval = true;
--- 581,590 ----
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ var->value = CStringGetTextDatum("INSTEAD OF");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE, AFTER or INSTEAD OF");
var->isnull = false;
var->freeval = true;
*** ./src/pl/plpython/expected/plpython_trigger.out.orig 2010-09-04 11:22:40.000000000 +0100
--- ./src/pl/plpython/expected/plpython_trigger.out 2010-09-04 11:34:21.000000000 +0100
***************
*** 294,304 ****
CONTEXT: PL/Python function "trigger_data"
NOTICE: TD[when] => BEFORE
CONTEXT: PL/Python function "trigger_data"
DROP FUNCTION trigger_data() CASCADE;
! NOTICE: drop cascades to 3 other objects
! DETAIL: drop cascades to trigger show_trigger_data_trig_before on table trigger_test
! drop cascades to trigger show_trigger_data_trig_after on table trigger_test
! drop cascades to trigger show_trigger_data_trig_stmt on table trigger_test
--
-- trigger error handling
--
--- 294,374 ----
CONTEXT: PL/Python function "trigger_data"
NOTICE: TD[when] => BEFORE
CONTEXT: PL/Python function "trigger_data"
+ DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
+ DROP TRIGGER show_trigger_data_trig_before on trigger_test;
+ DROP TRIGGER show_trigger_data_trig_after on trigger_test;
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => INSERT
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => {'i': 2, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => None
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
+ update trigger_test_view set v = 'update' where i = 1;
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => UPDATE
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => {'i': 1, 'v': 'update'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
+ delete from trigger_test_view;
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => DELETE
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => None
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
DROP FUNCTION trigger_data() CASCADE;
! NOTICE: drop cascades to trigger show_trigger_data_trig on view trigger_test_view
! DROP VIEW trigger_test_view;
! delete from trigger_test;
--
-- trigger error handling
--
*** ./src/pl/plpython/plpython.c.orig 2010-08-28 17:30:36.000000000 +0100
--- ./src/pl/plpython/plpython.c 2010-08-30 23:39:52.000000000 +0100
***************
*** 846,851 ****
--- 846,853 ----
pltwhen = PyString_FromString("BEFORE");
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
pltwhen = PyString_FromString("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ pltwhen = PyString_FromString("INSTEAD OF");
else
{
elog(ERROR, "unrecognized WHEN tg_event: %u", tdata->tg_event);
*** ./src/pl/plpython/sql/plpython_trigger.sql.orig 2010-09-04 11:22:22.000000000 +0100
--- ./src/pl/plpython/sql/plpython_trigger.sql 2010-09-04 11:33:54.000000000 +0100
***************
*** 99,106 ****
delete from trigger_test;
truncate table trigger_test;
! DROP FUNCTION trigger_data() CASCADE;
--
-- trigger error handling
--- 99,122 ----
delete from trigger_test;
truncate table trigger_test;
! DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
! DROP TRIGGER show_trigger_data_trig_before on trigger_test;
! DROP TRIGGER show_trigger_data_trig_after on trigger_test;
!
! insert into trigger_test values(1,'insert');
! CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
!
! CREATE TRIGGER show_trigger_data_trig
! INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
! FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i = 1;
+ delete from trigger_test_view;
+
+ DROP FUNCTION trigger_data() CASCADE;
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
--
-- trigger error handling
*** ./src/pl/tcl/expected/pltcl_queries_1.out.orig 2010-09-04 11:45:57.000000000 +0100
--- ./src/pl/tcl/expected/pltcl_queries_1.out 2010-09-04 22:06:23.000000000 +0100
***************
*** 196,201 ****
--- 196,237 ----
NOTICE: TG_table_schema: public
NOTICE: TG_when: BEFORE
NOTICE: args: {23 skidoo}
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: NEW: {i: 2, v: insert}
+ NOTICE: OLD: {}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: INSERT
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ update trigger_test_view set v = 'update' where i=1;
+ NOTICE: NEW: {i: 1, v: update}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: UPDATE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ delete from trigger_test_view;
+ NOTICE: NEW: {}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: DELETE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
update trigger_test set v = 'update' where i = 1;
NOTICE: NEW: {i: 1, v: update}
NOTICE: OLD: {i: 1, v: insert}
*** ./src/pl/tcl/expected/pltcl_queries.out.orig 2010-09-04 11:38:26.000000000 +0100
--- ./src/pl/tcl/expected/pltcl_queries.out 2010-09-04 22:05:50.000000000 +0100
***************
*** 196,201 ****
--- 196,237 ----
NOTICE: TG_table_schema: public
NOTICE: TG_when: BEFORE
NOTICE: args: {23 skidoo}
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: NEW: {i: 2, v: insert}
+ NOTICE: OLD: {}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: INSERT
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ update trigger_test_view set v = 'update' where i=1;
+ NOTICE: NEW: {i: 1, v: update}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: UPDATE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ delete from trigger_test_view;
+ NOTICE: NEW: {}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: DELETE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
update trigger_test set v = 'update' where i = 1;
NOTICE: NEW: {i: 1, v: update}
NOTICE: OLD: {i: 1, v: insert}
*** ./src/pl/tcl/expected/pltcl_setup.out.orig 2010-09-04 11:37:59.000000000 +0100
--- ./src/pl/tcl/expected/pltcl_setup.out 2010-09-04 11:43:22.000000000 +0100
***************
*** 51,56 ****
--- 51,57 ----
-- dump trigger data
CREATE TABLE trigger_test
(i int, v text );
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
if { [info exists TG_relid] } {
***************
*** 85,90 ****
--- 86,94 ----
CREATE TRIGGER show_trigger_data_trig
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ CREATE TRIGGER show_trigger_data_view_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
--
-- Trigger function on every change to T_pkey1
--
*** ./src/pl/tcl/pltcl.c.orig 2010-07-31 12:26:53.000000000 +0100
--- ./src/pl/tcl/pltcl.c 2010-08-30 23:40:13.000000000 +0100
***************
*** 822,827 ****
--- 822,829 ----
Tcl_DStringAppendElement(&tcl_cmd, "BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
Tcl_DStringAppendElement(&tcl_cmd, "AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ Tcl_DStringAppendElement(&tcl_cmd, "INSTEAD OF");
else
elog(ERROR, "unrecognized WHEN tg_event: %u", trigdata->tg_event);
*** ./src/pl/tcl/sql/pltcl_queries.sql.orig 2010-09-04 11:37:44.000000000 +0100
--- ./src/pl/tcl/sql/pltcl_queries.sql 2010-09-04 22:01:02.000000000 +0100
***************
*** 76,80 ****
--- 76,85 ----
-- show dump of trigger data
insert into trigger_test values(1,'insert');
+
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i=1;
+ delete from trigger_test_view;
+
update trigger_test set v = 'update' where i = 1;
delete from trigger_test;
*** ./src/pl/tcl/sql/pltcl_setup.sql.orig 2010-09-04 11:37:22.000000000 +0100
--- ./src/pl/tcl/sql/pltcl_setup.sql 2010-09-04 11:42:59.000000000 +0100
***************
*** 60,65 ****
--- 60,67 ----
CREATE TABLE trigger_test
(i int, v text );
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
if { [info exists TG_relid] } {
***************
*** 96,101 ****
--- 98,106 ----
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ CREATE TRIGGER show_trigger_data_view_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
--
-- Trigger function on every change to T_pkey1
*** ./src/test/regress/expected/triggers.out.orig 2010-08-15 15:55:21.000000000 +0100
--- ./src/test/regress/expected/triggers.out 2010-08-30 23:50:32.000000000 +0100
***************
*** 791,793 ****
--- 791,1419 ----
DROP TABLE min_updates_test;
DROP TABLE min_updates_test_oids;
+ --
+ -- Test triggers on views
+ --
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ ERROR: cannot insert into a view
+ HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+ UPDATE main_view SET b = 20 WHERE a = 50;
+ ERROR: cannot update a view
+ HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+ DELETE FROM main_view WHERE a = 50;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a = 51;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- VIEW trigger function
+ CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
+ LANGUAGE plpgsql AS $$
+ declare
+ argstr text := '';
+ begin
+ 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 '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr;
+
+ if TG_WHEN = 'INSTEAD OF' then
+ if TG_OP = 'INSERT' then
+ raise NOTICE 'NEW: %', NEW;
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ raise NOTICE 'OLD: %, NEW: %', OLD, NEW;
+ UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ raise NOTICE 'OLD: %', OLD;
+ DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end if;
+
+ RETURN NULL;
+ end;
+ $$;
+ -- Before row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ -- After row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ -- Truncate triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ -- INSTEAD OF triggers aren't allowed on tables
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ -- Don't support WHEN clauses with INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: cannot use a WHEN condition with an INSTEAD OF trigger
+ -- Don't support column-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: cannot use a column list with an INSTEAD OF trigger
+ -- Don't support statement-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: STATEMENT-level INSTEAD OF triggers are not supported
+ HINT: Use FOR EACH ROW for INSTEAD OF triggers
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+ -- Valid BEFORE statement VIEW triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
+ CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt');
+ CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt');
+ -- Valid AFTER statement VIEW triggers
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt');
+ CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt');
+ CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt');
+ \set QUIET false
+ -- Insert into view using trigger
+ INSERT INTO main_view VALUES (20, 30);
+ NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt)
+ NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins)
+ NOTICE: NEW: (20,30)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt)
+ INSERT 0 1
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+ NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt)
+ NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins)
+ NOTICE: NEW: (21,31)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt)
+ a | b
+ ----+----
+ 21 | 31
+ (1 row)
+
+ INSERT 0 1
+ -- Table trigger will prevent updates
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (20,30), NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 0
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (21,31), NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ a | b
+ ---+---
+ (0 rows)
+
+ UPDATE 0
+ -- Remove table trigger to allow updates
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ DROP TRIGGER
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (20,30), NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 1
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (21,31), NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ a | b
+ ----+----
+ 21 | 32
+ (1 row)
+
+ UPDATE 1
+ -- Before and after stmt triggers should fire even when no rows are affected
+ UPDATE main_view SET b = 0 WHERE false;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 0
+ -- Delete from view using trigger
+ DELETE FROM main_view WHERE a IN (20,21);
+ NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (21,10)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (20,31)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (21,32)
+ NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt)
+ DELETE 3
+ DELETE FROM main_view WHERE a = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (31,10)
+ NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt)
+ a | b
+ ----+----
+ 31 | 10
+ (1 row)
+
+ DELETE 1
+ \set QUIET true
+ -- Describe view should list triggers
+ \d main_view
+ View "public.main_view"
+ Column | Type | Modifiers
+ --------+---------+-----------
+ a | integer |
+ b | integer |
+ Triggers:
+ after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt')
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt')
+ before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt')
+ before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt')
+ instead_of_delete_trig INSTEAD OF DELETE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del')
+ instead_of_insert_trig INSTEAD OF INSERT ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd')
+
+ -- Test dropping view triggers
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d+ main_view
+ View "public.main_view"
+ Column | Type | Modifiers | Storage | Description
+ --------+---------+-----------+---------+-------------
+ a | integer | | plain |
+ b | integer | | plain |
+ View definition:
+ SELECT main_table.a, main_table.b
+ FROM main_table;
+ Triggers:
+ after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt')
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt')
+ before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt')
+ before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd')
+
+ DROP VIEW main_view;
+ --
+ -- Test triggers on a join view
+ --
+ CREATE TABLE country_table (
+ country_id serial primary key,
+ country_name text unique not null,
+ continent text not null
+ );
+ NOTICE: CREATE TABLE will create implicit sequence "country_table_country_id_seq" for serial column "country_table.country_id"
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "country_table_pkey" for table "country_table"
+ NOTICE: CREATE TABLE / UNIQUE will create implicit index "country_table_country_name_key" for table "country_table"
+ INSERT INTO country_table (country_name, continent)
+ VALUES ('Japan', 'Asia'),
+ ('UK', 'Europe'),
+ ('USA', 'North America')
+ RETURNING *;
+ country_id | country_name | continent
+ ------------+--------------+---------------
+ 1 | Japan | Asia
+ 2 | UK | Europe
+ 3 | USA | North America
+ (3 rows)
+
+ CREATE TABLE city_table (
+ city_id serial primary key,
+ city_name text not null,
+ population bigint,
+ country_id int references country_table
+ );
+ NOTICE: CREATE TABLE will create implicit sequence "city_table_city_id_seq" for serial column "city_table.city_id"
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "city_table_pkey" for table "city_table"
+ CREATE VIEW city_view AS
+ SELECT city_id, city_name, population, country_name, continent
+ FROM city_table ci
+ LEFT JOIN country_table co ON co.country_id = ci.country_id;
+ CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS NOT NULL then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+ else
+ NEW.continent := NULL;
+ end if;
+
+ if NEW.city_id IS NOT NULL then
+ INSERT INTO city_table
+ VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id);
+ else
+ INSERT INTO city_table(city_name, population, country_id)
+ VALUES(NEW.city_name, NEW.population, ctry_id)
+ RETURNING city_id INTO NEW.city_id;
+ end if;
+
+ RETURN NEW;
+ end;
+ $$;
+ CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_insert();
+ CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ DELETE FROM city_table WHERE city_id = OLD.city_id;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end;
+ $$;
+ CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_delete();
+ CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS DISTINCT FROM OLD.country_name then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population,
+ country_id = ctry_id
+ WHERE city_id = OLD.city_id;
+ else
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population
+ WHERE city_id = OLD.city_id;
+ NEW.continent := OLD.continent;
+ end if;
+
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end;
+ $$;
+ CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_update();
+ -- INSERT .. RETURNING
+ \set QUIET false
+ INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 2 | London | 7556900 | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+---------------+------------+--------------+---------------
+ 3 | Washington DC | | USA | North America
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 123456 | New York | | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ INSERT 0 1
+ -- UPDATE .. RETURNING
+ UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error
+ ERROR: No such country: "Japon"
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match
+ UPDATE 0
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | | Japan | Asia
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | 13010279 | Japan | Asia
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 123456 | New York | | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+---------------
+ 123456 | New York | 8391881 | USA | North America
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2
+ WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *;
+ city_id | city_name | population | country_name | continent | city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------+---------+------------+------------+--------------+-----------
+ 2 | London | 7556900 | UK | Europe | 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ -- DELETE .. RETURNING
+ DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ DELETE 1
+ \set QUIET true
+ -- read-only view with WHERE clause
+ CREATE VIEW european_city_view AS
+ SELECT * FROM city_view WHERE continent = 'Europe';
+ SELECT count(*) FROM european_city_view;
+ count
+ -------
+ 1
+ (1 row)
+
+ CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql
+ AS 'begin RETURN NULL; end';
+ CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE
+ ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn();
+ \set QUIET false
+ INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z');
+ INSERT 0 0
+ UPDATE european_city_view SET population = 10000;
+ UPDATE 0
+ DELETE FROM european_city_view;
+ DELETE 0
+ \set QUIET true
+ -- rules bypassing no-op triggers
+ CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view
+ DO INSTEAD INSERT INTO city_view
+ VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent)
+ RETURNING *;
+ CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view
+ DO INSTEAD UPDATE city_view SET
+ city_name = NEW.city_name,
+ population = NEW.population,
+ country_name = NEW.country_name
+ WHERE city_id = OLD.city_id
+ RETURNING NEW.*;
+ CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view
+ DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *;
+ -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are
+ \set QUIET false
+ INSERT INTO european_city_view(city_name, country_name)
+ VALUES ('Cambridge', 'USA') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+---------------
+ 4 | Cambridge | | USA | North America
+ (1 row)
+
+ INSERT 0 1
+ UPDATE european_city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge';
+ UPDATE 0
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge';
+ DELETE 0
+ -- UPDATE and DELETE via rule and trigger
+ UPDATE city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE european_city_view SET population = 122800
+ WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | 122800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | 122800 | UK | Europe
+ (1 row)
+
+ DELETE 1
+ -- join UPDATE test
+ UPDATE city_view v SET population = 599657
+ FROM city_table ci, country_table co
+ WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA'
+ AND v.city_id = ci.city_id AND v.country_name = co.country_name
+ RETURNING co.country_id, v.country_name,
+ v.city_id, v.city_name, v.population;
+ country_id | country_name | city_id | city_name | population
+ ------------+--------------+---------+---------------+------------
+ 3 | USA | 3 | Washington DC | 599657
+ (1 row)
+
+ UPDATE 1
+ \set QUIET true
+ SELECT * FROM city_view;
+ city_id | city_name | population | country_name | continent
+ ---------+---------------+------------+--------------+---------------
+ 1 | Tokyo | 13010279 | Japan | Asia
+ 123456 | New York | 8391881 | USA | North America
+ 2 | London | 7556900 | UK | Europe
+ 3 | Washington DC | 599657 | USA | North America
+ (4 rows)
+
+ DROP TABLE city_table CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to view city_view
+ drop cascades to view european_city_view
+ DROP TABLE country_table;
*** ./src/test/regress/regress.c.orig 2010-07-31 12:28:32.000000000 +0100
--- ./src/test/regress/regress.c 2010-08-15 12:52:20.000000000 +0100
***************
*** 489,495 ****
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
--- 489,495 ----
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
*** ./src/test/regress/sql/triggers.sql.orig 2010-08-19 09:07:19.000000000 +0100
--- ./src/test/regress/sql/triggers.sql 2010-08-30 23:51:39.000000000 +0100
***************
*** 578,580 ****
--- 578,937 ----
DROP TABLE min_updates_test;
DROP TABLE min_updates_test_oids;
+
+ --
+ -- Test triggers on views
+ --
+
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ UPDATE main_view SET b = 20 WHERE a = 50;
+ DELETE FROM main_view WHERE a = 50;
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a = 51;
+
+ -- VIEW trigger function
+ CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
+ LANGUAGE plpgsql AS $$
+ declare
+ argstr text := '';
+ begin
+ 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 '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr;
+
+ if TG_WHEN = 'INSTEAD OF' then
+ if TG_OP = 'INSERT' then
+ raise NOTICE 'NEW: %', NEW;
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ raise NOTICE 'OLD: %, NEW: %', OLD, NEW;
+ UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ raise NOTICE 'OLD: %', OLD;
+ DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end if;
+
+ RETURN NULL;
+ end;
+ $$;
+
+ -- Before row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ -- After row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ -- Truncate triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ -- INSTEAD OF triggers aren't allowed on tables
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+
+ -- Don't support WHEN clauses with INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Don't support column-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Don't support statement-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+
+ -- Valid BEFORE statement VIEW triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
+
+ CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt');
+
+ CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt');
+
+ -- Valid AFTER statement VIEW triggers
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt');
+
+ CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt');
+
+ CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt');
+
+ \set QUIET false
+
+ -- Insert into view using trigger
+ INSERT INTO main_view VALUES (20, 30);
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+
+ -- Table trigger will prevent updates
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+
+ -- Remove table trigger to allow updates
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+
+ -- Before and after stmt triggers should fire even when no rows are affected
+ UPDATE main_view SET b = 0 WHERE false;
+
+ -- Delete from view using trigger
+ DELETE FROM main_view WHERE a IN (20,21);
+ DELETE FROM main_view WHERE a = 31 RETURNING a, b;
+
+ \set QUIET true
+
+ -- Describe view should list triggers
+ \d main_view
+
+ -- Test dropping view triggers
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d+ main_view
+ DROP VIEW main_view;
+
+ --
+ -- Test triggers on a join view
+ --
+ CREATE TABLE country_table (
+ country_id serial primary key,
+ country_name text unique not null,
+ continent text not null
+ );
+
+ INSERT INTO country_table (country_name, continent)
+ VALUES ('Japan', 'Asia'),
+ ('UK', 'Europe'),
+ ('USA', 'North America')
+ RETURNING *;
+
+ CREATE TABLE city_table (
+ city_id serial primary key,
+ city_name text not null,
+ population bigint,
+ country_id int references country_table
+ );
+
+ CREATE VIEW city_view AS
+ SELECT city_id, city_name, population, country_name, continent
+ FROM city_table ci
+ LEFT JOIN country_table co ON co.country_id = ci.country_id;
+
+ CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS NOT NULL then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+ else
+ NEW.continent := NULL;
+ end if;
+
+ if NEW.city_id IS NOT NULL then
+ INSERT INTO city_table
+ VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id);
+ else
+ INSERT INTO city_table(city_name, population, country_id)
+ VALUES(NEW.city_name, NEW.population, ctry_id)
+ RETURNING city_id INTO NEW.city_id;
+ end if;
+
+ RETURN NEW;
+ end;
+ $$;
+
+ CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_insert();
+
+ CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ DELETE FROM city_table WHERE city_id = OLD.city_id;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end;
+ $$;
+
+ CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_delete();
+
+ CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS DISTINCT FROM OLD.country_name then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population,
+ country_id = ctry_id
+ WHERE city_id = OLD.city_id;
+ else
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population
+ WHERE city_id = OLD.city_id;
+ NEW.continent := OLD.continent;
+ end if;
+
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end;
+ $$;
+
+ CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_update();
+
+ -- INSERT .. RETURNING
+ \set QUIET false
+
+ INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *;
+ INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *;
+ INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *;
+ INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *;
+ INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *;
+
+ -- UPDATE .. RETURNING
+ UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK
+
+ UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *;
+ UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *;
+ UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *;
+ UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *;
+ UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2
+ WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *;
+
+ -- DELETE .. RETURNING
+ DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *;
+
+ \set QUIET true
+
+ -- read-only view with WHERE clause
+ CREATE VIEW european_city_view AS
+ SELECT * FROM city_view WHERE continent = 'Europe';
+ SELECT count(*) FROM european_city_view;
+
+ CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql
+ AS 'begin RETURN NULL; end';
+
+ CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE
+ ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn();
+
+ \set QUIET false
+
+ INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z');
+ UPDATE european_city_view SET population = 10000;
+ DELETE FROM european_city_view;
+
+ \set QUIET true
+
+ -- rules bypassing no-op triggers
+ CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view
+ DO INSTEAD INSERT INTO city_view
+ VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent)
+ RETURNING *;
+
+ CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view
+ DO INSTEAD UPDATE city_view SET
+ city_name = NEW.city_name,
+ population = NEW.population,
+ country_name = NEW.country_name
+ WHERE city_id = OLD.city_id
+ RETURNING NEW.*;
+
+ CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view
+ DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *;
+
+ -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are
+ \set QUIET false
+
+ INSERT INTO european_city_view(city_name, country_name)
+ VALUES ('Cambridge', 'USA') RETURNING *;
+ UPDATE european_city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge';
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge';
+
+ -- UPDATE and DELETE via rule and trigger
+ UPDATE city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge' RETURNING *;
+ UPDATE european_city_view SET population = 122800
+ WHERE city_name = 'Cambridge' RETURNING *;
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *;
+
+ -- join UPDATE test
+ UPDATE city_view v SET population = 599657
+ FROM city_table ci, country_table co
+ WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA'
+ AND v.city_id = ci.city_id AND v.country_name = co.country_name
+ RETURNING co.country_id, v.country_name,
+ v.city_id, v.city_name, v.population;
+
+ \set QUIET true
+
+ SELECT * FROM city_view;
+
+ DROP TABLE city_table CASCADE;
+ DROP TABLE country_table;
On Sep 5, 2010, at 3:09 AM, Dean Rasheed wrote:
On 15 August 2010 18:38, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Here is a WIP patch implementing triggers on VIEWs ... <snip>
There are still a number of things left todo:
- extend ALTER VIEW with enable/disable trigger commands
- much more testing
- documentationAttached is an updated patch with more tests and docs, and a few minor
At least for me, there are some portions of the docs which could use some formatting changes in order to not be confusing grammatically; e.g., this was confusing to me on the first read:
- trigger name. In the case of before triggers, the
+ trigger name. In the case of before and instead of triggers, the
I realize this lack of formatting was inherited from the existing docs, but this would make more sense to me if this (and presumably the other related instances of "before" and "after") were set apart with <literal></> or similar. This is already in use in some places in this patch, so seems like the correct markup.
Regards,
David
--
David Christensen
End Point Corporation
david@endpoint.com
On 7 September 2010 02:03, David Christensen <david@endpoint.com> wrote:
On Sep 5, 2010, at 3:09 AM, Dean Rasheed wrote:
On 15 August 2010 18:38, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Here is a WIP patch implementing triggers on VIEWs ... <snip>
There are still a number of things left todo:
- extend ALTER VIEW with enable/disable trigger commands
- much more testing
- documentationAttached is an updated patch with more tests and docs, and a few minor
At least for me, there are some portions of the docs which could use some formatting changes in order to not be confusing grammatically; e.g., this was confusing to me on the first read:
- trigger name. In the case of before triggers, the + trigger name. In the case of before and instead of triggers, theI realize this lack of formatting was inherited from the existing docs, but this would make more sense to me if this (and presumably the other related instances of "before" and "after") were set apart with <literal></> or similar. This is already in use in some places in this patch, so seems like the correct markup.
Yeah, I think you're right. That chapter is the first place in the
manual where the concepts of before/after/instead of are introduced.
The first time it mentions them it uses <firstterm>, but then it
doesn't use any markup for them for the remainder of the chapter. It
looks like the rest of the manual uses <literal>+uppercase wherever
they're mentioned, which does help readability, so it would make sense
to bring the rest of that chapter into line with that convention.
Thanks for looking at this.
Cheers,
Dean
On 7 September 2010 08:43, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On 7 September 2010 02:03, David Christensen <david@endpoint.com> wrote:
On Sep 5, 2010, at 3:09 AM, Dean Rasheed wrote:
On 15 August 2010 18:38, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Here is a WIP patch implementing triggers on VIEWs ... <snip>
There are still a number of things left todo:
- extend ALTER VIEW with enable/disable trigger commands
- much more testing
- documentationAttached is an updated patch with more tests and docs, and a few minor
At least for me, there are some portions of the docs which could use some formatting changes in order to not be confusing grammatically; e.g., this was confusing to me on the first read:
- trigger name. In the case of before triggers, the + trigger name. In the case of before and instead of triggers, theI realize this lack of formatting was inherited from the existing docs, but this would make more sense to me if this (and presumably the other related instances of "before" and "after") were set apart with <literal></> or similar. This is already in use in some places in this patch, so seems like the correct markup.
Yeah, I think you're right. That chapter is the first place in the
manual where the concepts of before/after/instead of are introduced.
The first time it mentions them it uses <firstterm>, but then it
doesn't use any markup for them for the remainder of the chapter. It
looks like the rest of the manual uses <literal>+uppercase wherever
they're mentioned, which does help readability, so it would make sense
to bring the rest of that chapter into line with that convention.Thanks for looking at this.
Cheers,
Dean
Here's an updated version with improved formatting and a few minor
wording changes to the triggers chapter.
Regards,
Dean
Attachments:
view_triggers.patchapplication/octet-stream; name=view_triggers.patchDownload
*** ./contrib/spi/autoinc.c.orig 2010-07-31 12:06:47.000000000 +0100
--- ./contrib/spi/autoinc.c 2010-08-15 12:55:08.000000000 +0100
***************
*** 38,44 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
--- 38,44 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
*** ./contrib/spi/insert_username.c.orig 2010-07-31 12:07:44.000000000 +0100
--- ./contrib/spi/insert_username.c 2010-08-15 12:56:59.000000000 +0100
***************
*** 41,47 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
--- 41,47 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
*** ./contrib/spi/moddatetime.c.orig 2010-07-31 12:10:47.000000000 +0100
--- ./contrib/spi/moddatetime.c 2010-08-15 12:57:35.000000000 +0100
***************
*** 47,53 ****
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
--- 47,53 ----
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
*** ./contrib/spi/timetravel.c.orig 2010-07-31 12:13:01.000000000 +0100
--- ./contrib/spi/timetravel.c 2010-08-15 16:42:48.000000000 +0100
***************
*** 122,128 ****
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
--- 122,128 ----
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
*** ./doc/src/sgml/catalogs.sgml.orig 2010-09-04 14:38:30.000000000 +0100
--- ./doc/src/sgml/catalogs.sgml 2010-09-04 15:04:08.000000000 +0100
***************
*** 4711,4717 ****
</indexterm>
<para>
! The catalog <structname>pg_trigger</structname> stores triggers on tables.
See <xref linkend="sql-createtrigger">
for more information.
</para>
--- 4711,4717 ----
</indexterm>
<para>
! The catalog <structname>pg_trigger</structname> stores triggers on tables and views.
See <xref linkend="sql-createtrigger">
for more information.
</para>
*** ./doc/src/sgml/information_schema.sgml.orig 2010-08-31 11:57:59.000000000 +0100
--- ./doc/src/sgml/information_schema.sgml 2010-08-31 12:59:42.000000000 +0100
***************
*** 4885,4892 ****
<para>
The view <literal>triggers</literal> contains all triggers defined
! in the current database on tables that the current user owns or has
! some non-SELECT privilege on.
</para>
<table>
--- 4885,4892 ----
<para>
The view <literal>triggers</literal> contains all triggers defined
! in the current database on tables and views that the current user owns
! or has some non-SELECT privilege on.
</para>
<table>
***************
*** 4990,4997 ****
<entry><literal>condition_timing</literal></entry>
<entry><type>character_data</type></entry>
<entry>
! Time at which the trigger fires (<literal>BEFORE</literal> or
! <literal>AFTER</literal>)
</entry>
</row>
--- 4990,4997 ----
<entry><literal>condition_timing</literal></entry>
<entry><type>character_data</type></entry>
<entry>
! Time at which the trigger fires (<literal>BEFORE</literal>,
! <literal>AFTER</literal> or <literal>INSTEAD OF</literal>)
</entry>
</row>
***************
*** 5562,5580 ****
<row>
<entry><literal>is_trigger_updatable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>Applies to a feature not available in <productname>PostgreSQL</></entry>
</row>
<row>
<entry><literal>is_trigger_deletable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>Applies to a feature not available in <productname>PostgreSQL</></entry>
</row>
<row>
<entry><literal>is_trigger_insertable_into</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>Applies to a feature not available in <productname>PostgreSQL</></entry>
</row>
</tbody>
</tgroup>
--- 5562,5589 ----
<row>
<entry><literal>is_trigger_updatable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>
! <literal>YES</> if the view has an <literal>INSTEAD OF</>
! <command>UPDATE</> trigger defined on it, <literal>NO</> if not
! </entry>
</row>
<row>
<entry><literal>is_trigger_deletable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>
! <literal>YES</> if the view has an <literal>INSTEAD OF</>
! <command>DELETE</> trigger defined on it, <literal>NO</> if not
! </entry>
</row>
<row>
<entry><literal>is_trigger_insertable_into</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>
! <literal>YES</> if the view has an <literal>INSTEAD OF</>
! <command>INSERT</> trigger defined on it, <literal>NO</> if not
! </entry>
</row>
</tbody>
</tgroup>
*** ./doc/src/sgml/plperl.sgml.orig 2010-09-01 15:41:47.000000000 +0100
--- ./doc/src/sgml/plperl.sgml 2010-09-01 16:51:11.000000000 +0100
***************
*** 980,986 ****
<term><literal>$_TD->{when}</literal></term>
<listitem>
<para>
! When the trigger was called: <literal>BEFORE</literal>, <literal>AFTER</literal>, or <literal>UNKNOWN</literal>
</para>
</listitem>
</varlistentry>
--- 980,988 ----
<term><literal>$_TD->{when}</literal></term>
<listitem>
<para>
! When the trigger was called: <literal>BEFORE</literal>,
! <literal>AFTER</literal>, <literal>INSTEAD OF</literal> or
! <literal>UNKNOWN</literal>
</para>
</listitem>
</varlistentry>
*** ./doc/src/sgml/plpgsql.sgml.orig 2010-08-30 20:13:35.000000000 +0100
--- ./doc/src/sgml/plpgsql.sgml 2010-09-04 19:39:00.000000000 +0100
***************
*** 3112,3120 ****
<term><varname>TG_WHEN</varname></term>
<listitem>
<para>
! Data type <type>text</type>; a string of either
! <literal>BEFORE</literal> or <literal>AFTER</literal>
! depending on the trigger's definition.
</para>
</listitem>
</varlistentry>
--- 3112,3120 ----
<term><varname>TG_WHEN</varname></term>
<listitem>
<para>
! Data type <type>text</type>; a string of
! <literal>BEFORE</literal>, <literal>AFTER</literal> or
! <literal>INSTEAD OF</literal> depending on the trigger's definition.
</para>
</listitem>
</varlistentry>
***************
*** 3239,3244 ****
--- 3239,3259 ----
</para>
<para>
+ <literal>INSTEAD OF</> triggers (which are always row-level triggers,
+ and may only be used on views) can return null to signal that they did
+ not perform any updates, and that the rest of the operation for this
+ row should be skipped (i.e., subsequent triggers are not fired, and the
+ <command>INSERT</>/<command>UPDATE</>/<command>DELETE</> did not occur
+ for this row). Otherwise a nonnull value should be returned, to signal
+ that the trigger performed the requested operation. For
+ <command>INSERT</> and <command>UPDATE</> operations, the return value
+ should be <varname>NEW</>, which the trigger function may modify to
+ support <command>INSERT RETURNING</> and <command>UPDATE RETURNING</>.
+ For <command>DELETE</> operations, the return value should be
+ <varname>OLD</>.
+ </para>
+
+ <para>
The return value of a row-level trigger
fired <literal>AFTER</literal> or a statement-level trigger
fired <literal>BEFORE</> or <literal>AFTER</> is
***************
*** 3354,3359 ****
--- 3369,3453 ----
</programlisting>
</example>
+ <para>
+ A variation of the previous example uses a view joining the main table
+ to the audit table, to show when each entry was last modified. This
+ approach still records the full audit trail of changes to the table,
+ but also presents a simplified view of the audit trail, showing just
+ the last modified timestamp derived from the audit trail for each entry.
+ <xref linkend="plpgsql-view-trigger-audit-example"> shows an example
+ of an audit trigger on a view in <application>PL/pgSQL</application>.
+ </para>
+
+ <example id="plpgsql-view-trigger-audit-example">
+ <title>A <application>PL/pgSQL</application> View Trigger Procedure For Auditing</title>
+
+ <para>
+ This example uses a trigger on the view to make it updatable, and
+ ensure that any insert, update or delete of a row in the view is
+ recorded (i.e., audited) in the emp_audit table. The current time
+ and user name are recorded, together with the type of operation
+ performed, and the view displays the last modified time of each row.
+ </para>
+
+ <programlisting>
+ CREATE TABLE emp (
+ empname text PRIMARY KEY,
+ salary integer
+ );
+
+ CREATE TABLE emp_audit(
+ operation char(1) NOT NULL,
+ userid text NOT NULL,
+ empname text NOT NULL,
+ salary integer,
+ stamp timestamp NOT NULL
+ );
+
+ CREATE VIEW emp_view AS
+ SELECT e.empname,
+ e.salary,
+ max(ea.stamp) AS last_updated
+ FROM emp e
+ LEFT JOIN emp_audit ea ON ea.empname = e.empname
+ GROUP BY 1, 2;
+
+ CREATE OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$
+ BEGIN
+ --
+ -- Perform the required operation on emp, and create a row in emp_audit
+ -- to reflect the change made to emp.
+ --
+ IF (TG_OP = 'DELETE') THEN
+ DELETE FROM emp WHERE empname = OLD.empname;
+ IF NOT FOUND THEN RETURN NULL; END IF;
+
+ OLD.last_updated = now();
+ INSERT INTO emp_audit VALUES('D', user, OLD.*);
+ RETURN OLD;
+ ELSIF (TG_OP = 'UPDATE') THEN
+ UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;
+ IF NOT FOUND THEN RETURN NULL; END IF;
+
+ NEW.last_updated = now();
+ INSERT INTO emp_audit VALUES('U', user, NEW.*);
+ RETURN NEW;
+ ELSIF (TG_OP = 'INSERT') THEN
+ INSERT INTO emp VALUES(NEW.empname, NEW.salary);
+
+ NEW.last_updated = now();
+ INSERT INTO emp_audit VALUES('I', user, NEW.*);
+ RETURN NEW;
+ END IF;
+ END;
+ $$ LANGUAGE plpgsql;
+
+ CREATE TRIGGER emp_audit
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
+ FOR EACH ROW EXECUTE PROCEDURE update_emp_view();
+ </programlisting>
+ </example>
+
<para>
One use of triggers is to maintain a summary table
of another table. The resulting summary can be used in place of the
*** ./doc/src/sgml/plpython.sgml.orig 2010-09-01 18:01:23.000000000 +0100
--- ./doc/src/sgml/plpython.sgml 2010-09-01 18:14:17.000000000 +0100
***************
*** 751,757 ****
<listitem>
<para>
contains one of <literal>BEFORE</>, <literal>AFTER</>,
! or <literal>UNKNOWN</>.
</para>
</listitem>
</varlistentry>
--- 751,757 ----
<listitem>
<para>
contains one of <literal>BEFORE</>, <literal>AFTER</>,
! <literal>INSTEAD OF</> or <literal>UNKNOWN</>.
</para>
</listitem>
</varlistentry>
***************
*** 827,838 ****
</para>
<para>
! If <literal>TD["when"]</literal> is <literal>BEFORE</> and
<literal>TD["level"]</literal> is <literal>ROW</>, you can
return <literal>None</literal> or <literal>"OK"</literal> from the
Python function to indicate the row is unmodified,
! <literal>"SKIP"</> to abort the event, or <literal>"MODIFY"</> to
! indicate you've modified the row.
Otherwise the return value is ignored.
</para>
</sect1>
--- 827,840 ----
</para>
<para>
! If <literal>TD["when"]</literal> is <literal>BEFORE</> or
! <literal>INSTEAD OF</> and
<literal>TD["level"]</literal> is <literal>ROW</>, you can
return <literal>None</literal> or <literal>"OK"</literal> from the
Python function to indicate the row is unmodified,
! <literal>"SKIP"</> to abort the event, or if <literal>TD["event"]</>
! is <command>INSERT</> or <command>UPDATE</> you can return
! <literal>"MODIFY"</> to indicate you've modified the new row.
Otherwise the return value is ignored.
</para>
</sect1>
*** ./doc/src/sgml/pltcl.sgml.orig 2010-09-01 15:07:44.000000000 +0100
--- ./doc/src/sgml/pltcl.sgml 2010-09-01 17:48:37.000000000 +0100
***************
*** 567,574 ****
<term><varname>$TG_when</varname></term>
<listitem>
<para>
! The string <literal>BEFORE</> or <literal>AFTER</> depending on the
! type of trigger event.
</para>
</listitem>
</varlistentry>
--- 567,574 ----
<term><varname>$TG_when</varname></term>
<listitem>
<para>
! The string <literal>BEFORE</>, <literal>AFTER</> or
! <literal>INSTEAD OF</> depending on the type of trigger event.
</para>
</listitem>
</varlistentry>
***************
*** 641,650 ****
the operation (<command>INSERT</>/<command>UPDATE</>/<command>DELETE</>) that fired the trigger will proceed
normally. <literal>SKIP</> tells the trigger manager to silently suppress
the operation for this row. If a list is returned, it tells PL/Tcl to
! return a modified row to the trigger manager that will be inserted
! instead of the one given in <varname>$NEW</>. (This works for <command>INSERT</> and <command>UPDATE</>
! only.) Needless to say that all this is only meaningful when the trigger
! is <literal>BEFORE</> and <command>FOR EACH ROW</>; otherwise the return value is ignored.
</para>
<para>
--- 641,654 ----
the operation (<command>INSERT</>/<command>UPDATE</>/<command>DELETE</>) that fired the trigger will proceed
normally. <literal>SKIP</> tells the trigger manager to silently suppress
the operation for this row. If a list is returned, it tells PL/Tcl to
! return a modified row to the trigger manager. This is only meaningful
! for row-level <literal>BEFORE</> <command>INSERT</> or <command>UPDATE</>
! triggers for which the modified row will be inserted instead of the one
! given in <varname>$NEW</>; or for row-level <literal>INSTEAD OF</>
! <command>INSERT</> or <command>UPDATE</> triggers where the returned row
! is used to support <command>INSERT RETURNING</> and
! <command>UPDATE RETURNING</> commands. The return value is ignored for
! other types of triggers.
</para>
<para>
*** ./doc/src/sgml/ref/create_rule.sgml.orig 2010-09-01 18:44:58.000000000 +0100
--- ./doc/src/sgml/ref/create_rule.sgml 2010-09-01 18:55:21.000000000 +0100
***************
*** 73,79 ****
sufficient for your purposes) to replace update actions on the view
with appropriate updates on other tables. If you want to support
<command>INSERT RETURNING</> and so on, then be sure to put a suitable
! <literal>RETURNING</> clause into each of these rules.
</para>
<para>
--- 73,81 ----
sufficient for your purposes) to replace update actions on the view
with appropriate updates on other tables. If you want to support
<command>INSERT RETURNING</> and so on, then be sure to put a suitable
! <literal>RETURNING</> clause into each of these rules. Alternatively,
! an updatable view may be implemented using <literal>INSTEAD OF</>
! triggers (see <xref linkend="sql-createtrigger">).
</para>
<para>
*** ./doc/src/sgml/ref/create_trigger.sgml.orig 2010-09-01 19:04:12.000000000 +0100
--- ./doc/src/sgml/ref/create_trigger.sgml 2010-09-04 18:14:52.000000000 +0100
***************
*** 21,27 ****
<refsynopsisdiv>
<synopsis>
! CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTER } { <replaceable class="PARAMETER">event</replaceable> [ OR ... ] }
ON <replaceable class="PARAMETER">table</replaceable> [ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( <replaceable class="parameter">condition</replaceable> ) ]
EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
--- 21,27 ----
<refsynopsisdiv>
<synopsis>
! CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTER | INSTEAD OF } { <replaceable class="PARAMETER">event</replaceable> [ OR ... ] }
ON <replaceable class="PARAMETER">table</replaceable> [ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( <replaceable class="parameter">condition</replaceable> ) ]
EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
***************
*** 33,53 ****
<para>
<command>CREATE TRIGGER</command> creates a new trigger. The
! trigger will be associated with the specified table and will
execute the specified function <replaceable
class="parameter">function_name</replaceable> when certain events occur.
</para>
<para>
! The trigger can be specified to fire either before the
operation is attempted on a row (before constraints are checked and
the <command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> is attempted) or after the operation has
completed (after constraints are checked and the
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> has completed). If the trigger fires
! before the event, the trigger can skip the operation for the
! current row, or change the row being inserted (for
<command>INSERT</command> and <command>UPDATE</command> operations
only). If the trigger fires after the event, all changes, including
the effects of other triggers, are <quote>visible</quote>
--- 33,54 ----
<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.
</para>
<para>
! The trigger can be specified to fire before the
operation is attempted on a row (before constraints are checked and
the <command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> is attempted); after the operation has
completed (after constraints are checked and the
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> has completed); or instead of the operation
! (in the case of inserts, updates or deletes on a view).
! If the trigger fires before or instead of the event, the trigger can skip
! the operation for the current row, or change the row being inserted (for
<command>INSERT</command> and <command>UPDATE</command> operations
only). If the trigger fires after the event, all changes, including
the effects of other triggers, are <quote>visible</quote>
***************
*** 68,86 ****
</para>
<para>
! In addition, triggers may be defined to fire for a
! <command>TRUNCATE</command>, though only
! <literal>FOR EACH STATEMENT</literal>.
</para>
<para>
! Also, a trigger definition can specify a Boolean <literal>WHEN</>
condition, which will be tested to see whether the trigger should
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
is not so useful for them since the condition cannot refer to any values
! in the table.
</para>
<para>
--- 69,144 ----
</para>
<para>
! Triggers that are specified to fire <literal>INSTEAD OF</> the trigger
! event must be marked <literal>FOR EACH ROW</>, and can only be defined
! on views. <literal>BEFORE</> and <literal>AFTER</> triggers on a view
! must be marked as <literal>FOR EACH STATEMENT</>.
</para>
<para>
! In addition, triggers may be defined to fire for a
! <command>TRUNCATE</command> (tables only), though only
! <literal>FOR EACH STATEMENT</literal>. The following table
! summarises which types of triggers may be used on tables and views:
! </para>
!
! <informaltable id="supported-trigger-types">
! <tgroup cols="4">
! <thead>
! <row>
! <entry>When</entry>
! <entry>Event</entry>
! <entry>Row-level</entry>
! <entry>Statement-level</entry>
! </row>
! </thead>
! <tbody>
! <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>
! </row>
! <row>
! <entry align="center"><command>TRUNCATE</></entry>
! <entry align="center">X</entry>
! <entry align="center">Tables</entry>
! </row>
! <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>
! </row>
! <row>
! <entry align="center"><command>TRUNCATE</></entry>
! <entry align="center">X</entry>
! <entry align="center">Tables</entry>
! </row>
! <row>
! <entry align="center" morerows="1"><literal>INSTEAD OF</></entry>
! <entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
! <entry align="center">Views</entry>
! <entry align="center">X</entry>
! </row>
! <row>
! <entry align="center"><command>TRUNCATE</></entry>
! <entry align="center">X</entry>
! <entry align="center">X</entry>
! </row>
! </tbody>
! </tgroup>
! </informaltable>
!
! <para>
! Also, most trigger definitions can specify a Boolean <literal>WHEN</>
condition, which will be tested to see whether the trigger should
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
is not so useful for them since the condition cannot refer to any values
! in the table. <literal>INSTEAD OF</> triggers cannot be used with
! <literal>WHEN</> conditions.
</para>
<para>
***************
*** 116,125 ****
<varlistentry>
<term><literal>BEFORE</literal></term>
<term><literal>AFTER</literal></term>
<listitem>
<para>
! Determines whether the function is called before or after the
! event.
</para>
</listitem>
</varlistentry>
--- 174,184 ----
<varlistentry>
<term><literal>BEFORE</literal></term>
<term><literal>AFTER</literal></term>
+ <term><literal>INSTEAD OF</literal></term>
<listitem>
<para>
! Determines whether the function is called before, after or instead of
! the event.
</para>
</listitem>
</varlistentry>
***************
*** 143,148 ****
--- 202,213 ----
The trigger will only fire if at least one of the listed columns
is mentioned as a target of the update.
</para>
+
+ <para>
+ <literal>INSTEAD OF</> triggers do not support lists of columns. An
+ <literal>INSTEAD OF</> trigger must be unconditional, and is fired
+ for each affected row in the view.
+ </para>
</listitem>
</varlistentry>
***************
*** 150,156 ****
<term><replaceable class="parameter">table</replaceable></term>
<listitem>
<para>
! The name (optionally schema-qualified) of the table the trigger
is for.
</para>
</listitem>
--- 215,221 ----
<term><replaceable class="parameter">table</replaceable></term>
<listitem>
<para>
! The name (optionally schema-qualified) of the table or view the trigger
is for.
</para>
</listitem>
***************
*** 189,194 ****
--- 254,264 ----
</para>
<para>
+ <literal>INSTEAD OF</> triggers do not support <literal>WHEN</>
+ conditions.
+ </para>
+
+ <para>
Currently, <literal>WHEN</literal> expressions cannot contain
subqueries.
</para>
***************
*** 326,331 ****
--- 396,411 ----
WHEN (OLD.* IS DISTINCT FROM NEW.*)
EXECUTE PROCEDURE log_account_update();
</programlisting>
+
+ Execute the function <function>view_insert_row</> for each row to insert
+ rows into a view:
+
+ <programlisting>
+ CREATE TRIGGER view_insert
+ INSTEAD OF INSERT ON my_view
+ FOR EACH ROW
+ EXECUTE PROCEDURE view_insert_row();
+ </programlisting>
</para>
<para>
***************
*** 396,402 ****
<para>
The ability to fire triggers for <command>TRUNCATE</command> is a
! <productname>PostgreSQL</> extension of the SQL standard.
</para>
</refsect1>
--- 476,485 ----
<para>
The ability to fire triggers for <command>TRUNCATE</command> is a
! <productname>PostgreSQL</> extension of the SQL standard, as is the
! ability to define statement-level triggers on views that fire
! <literal>BEFORE</> or <literal>AFTER</> <command>INSERT</>,
! <command>UPDATE</> and <command>DELETE</> commands.
</para>
</refsect1>
*** ./doc/src/sgml/rules.sgml.orig 2010-08-28 18:12:42.000000000 +0100
--- ./doc/src/sgml/rules.sgml 2010-08-31 14:02:45.000000000 +0100
***************
*** 168,176 ****
<para>
<command>DELETE</command> commands don't need a target list
because they don't produce any result. In fact, the planner will
! add a special <acronym>CTID</> entry to the empty target list, but
! this is after the rule system and will be discussed later; for the
! rule system, the target list is empty.
</para>
<para>
--- 168,178 ----
<para>
<command>DELETE</command> commands don't need a target list
because they don't produce any result. In fact, the planner will
! add a special <acronym>CTID</> entry to the empty target list (if the
! result relation is a table), but this is after the rule system and
! will be discussed later; for the rule system, the target list is empty.
! If the result relation is a view, the rule system will behave slightly
! differently, as described later.
</para>
<para>
***************
*** 189,198 ****
For <command>UPDATE</command> commands, the target list
describes the new rows that should replace the old ones. In the
rule system, it contains just the expressions from the <literal>SET
! column = expression</literal> part of the command. The planner will handle
! missing columns by inserting expressions that copy the values from
! the old row into the new one. And it will add the special
! <acronym>CTID</> entry just as for <command>DELETE</command>, too.
</para>
<para>
--- 191,202 ----
For <command>UPDATE</command> commands, the target list
describes the new rows that should replace the old ones. In the
rule system, it contains just the expressions from the <literal>SET
! column = expression</literal> part of the command. The planner will
! handle missing columns by inserting expressions that copy the values
! from the old row into the new one. And it will add the special
! <acronym>CTID</> entry, if the result relation is a table, just as
! for <command>DELETE</command>. If the result relation is a view,
! the rule system will behave slightly differently, as described later.
</para>
<para>
***************
*** 656,662 ****
<para>
Two details of the query tree aren't touched in the description of
view rules above. These are the command type and the result relation.
! In fact, view rules don't need this information.
</para>
<para>
--- 660,668 ----
<para>
Two details of the query tree aren't touched in the description of
view rules above. These are the command type and the result relation.
! In fact, the command type is not needed by view rules, but the result
! relation may affect the way in which the query rewriter works, because
! special care needs to be taken if the result relation is a view.
</para>
<para>
***************
*** 725,745 ****
</programlisting>
will do. But there is a little problem in
! <command>UPDATE</command>: The executor does not care what the
! results from the join it is doing are meant for. It just produces
! a result set of rows. The difference that one is a
! <command>SELECT</command> command and the other is an
! <command>UPDATE</command> is handled in the caller of the
! executor. The caller still knows (looking at the query tree) that
! this is an <command>UPDATE</command>, and it knows that this
! result should go into table <literal>t1</>. But which of the rows that are
! there has to be replaced by the new row?
</para>
<para>
! To resolve this problem, another entry is added to the target list
! in <command>UPDATE</command> (and also in
! <command>DELETE</command>) statements: the current tuple ID
(<acronym>CTID</>).<indexterm><primary>CTID</></>
This is a system column containing the
file block number and position in the block for the row. Knowing
--- 731,750 ----
</programlisting>
will do. But there is a little problem in
! <command>UPDATE</command>: The branch of the executor plan that does
! the join does not care what the results from the join it is doing are
! meant for. It just produces a result set of rows. The difference that
! one is a <command>SELECT</command> command and the other is an
! <command>UPDATE</command> is handled higher up in the executor, where
! it knows that this is an <command>UPDATE</command>, and it knows that
! this result should go into table <literal>t1</>. But which of the rows
! that are there has to be replaced by the new row?
</para>
<para>
! If the result relation is a table, this problem is resolved by adding
! another entry to the target list in <command>UPDATE</command> (and
! also in <command>DELETE</command>) statements: the current tuple ID
(<acronym>CTID</>).<indexterm><primary>CTID</></>
This is a system column containing the
file block number and position in the block for the row. Knowing
***************
*** 764,770 ****
<para>
Knowing all that, we can simply apply view rules in absolutely
! the same way to any command. There is no difference.
</para>
</sect2>
--- 769,777 ----
<para>
Knowing all that, we can simply apply view rules in absolutely
! the same way to any command. There is no difference, unless the view is
! the result relation, which must be handled differently (see
! <xref linkend="rules-views-update">).
</para>
</sect2>
***************
*** 803,818 ****
<para>
What happens if a view is named as the target relation for an
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command>? After doing the substitutions
! described above, we will have a query tree in which the result
! relation points at a subquery range-table entry. This will not
! work, so the rewriter throws an error if it sees it has produced
! such a thing.
</para>
<para>
! To change this, we can define rules that modify the behavior of
! these kinds of commands. This is the topic of the next section.
</para>
</sect2>
--- 810,859 ----
<para>
What happens if a view is named as the target relation for an
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command>? Simply doing the substitutions
! described above would give a query tree in which the result
! relation points at a subquery range-table entry, which would not
! work. The rewriter handles this case slightly differently, leaving
! the original view range-table entry as the result relation, and for
! <command>UPDATE</command> and <command>DELETE</command> operations,
! adding a new subquery range-table entry based on the view's query.
</para>
<para>
! Another problem that now arises is how to identify the rows to be
! updated in the result relation. Recall that when the result relation
! was a table, a special <acronym>CTID</> entry was added to the target
! list to identify the physical locations of the rows to be updated.
! This does not work if the result relation is a view, because a view
! typically does not have a <acronym>CTID</>, and its rows may not have
! unique physical locations. Instead, for an <command>UPDATE</command>
! or <command>DELETE</command> operation, a special <literal>wholerow</>
! entry is added to the target list, which expands to include all
! columns from the view. The executor does not know how to update the
! view; it relies on <literal>INSTEAD OF</> triggers for this. The
! executor uses the <literal>wholerow</> target list entry to pass the
! data from all the columns in the view to the triggers, to allow them
! to work out what to update.
! </para>
!
! <para>
! If there are no <literal>INSTEAD OF</> triggers to update the view,
! the executor will throw an error, because it cannot automatically
! update a view by itself. To change this, we can define rules that
! modify the behavior of <command>INSERT</command>,
! <command>UPDATE</command>, and <command>DELETE</command> commands on
! a view. These rules will rewrite the command, typically into a command
! that updates one or more tables, rather than views. This is the topic
! of the next section.
! </para>
!
! <para>
! Note that rules are evaluated first, rewriting the original query
! before it is planned and executed. Therefore, if a view has
! <literal>INSTEAD OF</> triggers as well as rules on <command>INSERT</>,
! <command>UPDATE</>, and <command>DELETE</>, then the rules will be
! evaluated first, and depending on the result, the triggers may not be
! used at all.
</para>
</sect2>
***************
*** 1868,1876 ****
</para>
<para>
! On the other hand, a trigger cannot be created on views because
! there is no real data in a view relation; however INSERT, UPDATE,
! and DELETE rules can be created on views.
</para>
<para>
--- 1909,1923 ----
</para>
<para>
! In this chapter, we focused on using rules to update views. All of
! the update rule examples in this chapter can also be implemented
! using <literal>INSTEAD OF</> triggers on the views (triggers defined
! to fire instead of the view update operations, with trigger functions
! that do the <command>INSERT</>, <command>UPDATE</> and
! <command>DELETE</> operations on the base tables). Writing such
! triggers is often easier than writing rules, particularly if complex
! logic is required to perform the update, however, it is possible that
! for bulk update operations, rules may perform better than triggers.
</para>
<para>
*** ./doc/src/sgml/trigger.sgml.orig 2010-08-28 18:09:13.000000000 +0100
--- ./doc/src/sgml/trigger.sgml 2010-09-07 21:06:24.000000000 +0100
***************
*** 33,39 ****
<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 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.
--- 33,43 ----
<para>
A trigger is a specification that the database should automatically
execute a particular function whenever a certain type of operation is
! performed.
! </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,
or once per <acronym>SQL</acronym> statement.
***************
*** 46,51 ****
--- 50,68 ----
</para>
<para>
+ On views, triggers can be defined to execute instead of
+ <command>INSERT</command>, <command>UPDATE</command>, or
+ <command>DELETE</command> operations, and are fired once per row that
+ needs to be modified in the view. It is the responsibility of the
+ trigger's function to perform the necessary modifications to the
+ underlying base tables and, where appropriate, return the modified
+ row as it appears in the view. Triggers can also be defined to
+ execute once per <acronym>SQL</acronym> statement, before or after
+ <command>INSERT</command>, <command>UPDATE</command>, or
+ <command>DELETE</command> operations.
+ </para>
+
+ <para>
The trigger function must be defined before the trigger itself can be
created. The trigger function must be declared as a
function taking no arguments and returning type <literal>trigger</>.
***************
*** 74,91 ****
two types of triggers are sometimes called <firstterm>row-level</>
triggers and <firstterm>statement-level</> triggers,
respectively. Triggers on <command>TRUNCATE</command> may only be
! defined at statement-level.
</para>
<para>
! Triggers are also classified as <firstterm>before</> triggers and
! <firstterm>after</> triggers.
! Statement-level before triggers naturally fire before the
! statement starts to do anything, while statement-level after
! triggers fire at the very end of the statement. Row-level before
triggers fire immediately before a particular row is operated on,
! while row-level after triggers fire at the end of the statement
! (but before any statement-level after triggers).
</para>
<para>
--- 91,120 ----
two types of triggers are sometimes called <firstterm>row-level</>
triggers and <firstterm>statement-level</> triggers,
respectively. Triggers on <command>TRUNCATE</command> may only be
! defined at statement-level. On views, triggers that fire before or
! after may only be defined at statement-level, and triggers that fire
! instead of an <command>INSERT</command>, <command>UPDATE</command>,
! or <command>DELETE</command> may only be defined at row-level.
</para>
<para>
! Triggers are also classified according to whether they fire
! <firstterm>before</>, <firstterm>after</> or
! <firstterm>instead of</> the operation. These are referred to
! as <literal>BEFORE</> triggers, <literal>AFTER</> triggers and
! <literal>INSTEAD OF</> triggers respectively.
! Statement-level <literal>BEFORE</> triggers naturally fire before the
! 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
! immediately as each row in the view is identified as needing to be
! operated on (after any statement-level <literal>BEFORE</> triggers,
! and before any statement-level <literal>AFTER</> triggers on the view).
</para>
<para>
***************
*** 117,124 ****
</listitem>
</itemizedlist>
! A row-level before trigger that does not intend to cause either of
! these behaviors must be careful to return as its result the same
row that was passed in (that is, the <varname>NEW</varname> row
for <command>INSERT</command> and <command>UPDATE</command>
triggers, the <varname>OLD</varname> row for
--- 146,153 ----
</listitem>
</itemizedlist>
! A row-level <literal>BEFORE</> trigger that does not intend to cause
! either of these behaviors must be careful to return as its result the same
row that was passed in (that is, the <varname>NEW</varname> row
for <command>INSERT</command> and <command>UPDATE</command>
triggers, the <varname>OLD</varname> row for
***************
*** 126,131 ****
--- 155,176 ----
</para>
<para>
+ A row-level <literal>INSTEAD OF</> trigger should either return
+ <symbol>NULL</> to indicate that it did not modify any data from
+ the view's underlying base tables, or it should return the view
+ row that was passed in (the <varname>NEW</varname> row
+ for <command>INSERT</command> and <command>UPDATE</command>
+ operations, or the <varname>OLD</varname> row for
+ <command>DELETE</command> operations). A nonnull return value is
+ used to signal that the trigger performed the necessary data
+ modifications in the view, and will cause the count of the number
+ of rows affected in the command status to be incremented. For
+ <command>INSERT</> and <command>UPDATE</> operations, the trigger
+ may modify the <varname>NEW</> row before returning it, to support
+ <command>INSERT RETURNING</> and <command>UPDATE RETURNING</>.
+ </para>
+
+ <para>
The return value is ignored for row-level triggers fired after an
operation, and so they can return <symbol>NULL</>.
</para>
***************
*** 133,143 ****
<para>
If more than one trigger is defined for the same event on the same
relation, the triggers will be fired in alphabetical order by
! trigger name. In the case of before triggers, the
! possibly-modified row returned by each trigger becomes the input
! to the next trigger. If any before trigger returns
<symbol>NULL</>, the operation is abandoned for that row and subsequent
! triggers are not fired.
</para>
<para>
--- 178,189 ----
<para>
If more than one trigger is defined for the same event on the same
relation, the triggers will be fired in alphabetical order by
! trigger name. In the case of <literal>BEFORE</> and
! <literal>INSTEAD OF</> triggers, the possibly-modified row returned by
! each trigger becomes the input to the next trigger. If any
! <literal>BEFORE</> or <literal>INSTEAD OF</> trigger returns
<symbol>NULL</>, the operation is abandoned for that row and subsequent
! triggers are not fired (for that row).
</para>
<para>
***************
*** 146,176 ****
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. (Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
! is not so useful for them.) In a before trigger, the <literal>WHEN</>
condition is evaluated just before the function is or would be executed,
so using <literal>WHEN</> is not materially different from testing the
same condition at the beginning of the trigger function. However, in
! an after trigger, the <literal>WHEN</> condition is evaluated just after
! the row update occurs, and it determines whether an event is queued to
! fire the trigger at the end of statement. So when an after trigger's
<literal>WHEN</> condition does not return true, it is not necessary
to queue an event nor to re-fetch the row at end of statement. This
can result in significant speedups in statements that modify many
rows, if the trigger only needs to be fired for a few of the rows.
</para>
<para>
! Typically, row before triggers are used for checking or
modifying the data that will be inserted or updated. For example,
! a before trigger might be used to insert the current time into a
<type>timestamp</type> column, or to check that two elements of the row are
! consistent. Row after triggers are most sensibly
used to propagate the updates to other tables, or make consistency
checks against other tables. The reason for this division of labor is
! that an after trigger can be certain it is seeing the final value of the
! row, while a before trigger cannot; there might be other before triggers
! firing after it. If you have no specific reason to make a trigger before
! or after, the before case is more efficient, since the information about
the operation doesn't have to be saved until end of statement.
</para>
--- 192,228 ----
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. (Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
! is not so useful for them.) In a <literal>BEFORE</> trigger, the
! <literal>WHEN</>
condition is evaluated just before the function is or would be executed,
so using <literal>WHEN</> is not materially different from testing the
same condition at the beginning of the trigger function. However, in
! an <literal>AFTER</> trigger, the <literal>WHEN</> condition is evaluated
! just after the row update occurs, and it determines whether an event is
! queued to fire the trigger at the end of statement. So when an
! <literal>AFTER</> trigger's
<literal>WHEN</> condition does not return true, it is not necessary
to queue an event nor to re-fetch the row at end of statement. This
can result in significant speedups in statements that modify many
rows, if the trigger only needs to be fired for a few of the rows.
+ <literal>INSTEAD OF</> triggers on views do not support
+ <literal>WHEN</> conditions.
</para>
<para>
! Typically, row-level <literal>BEFORE</> triggers are used for checking or
modifying the data that will be inserted or updated. For example,
! a <literal>BEFORE</> trigger might be used to insert the current time into a
<type>timestamp</type> column, or to check that two elements of the row are
! consistent. Row-level <literal>AFTER</> triggers are most sensibly
used to propagate the updates to other tables, or make consistency
checks against other tables. The reason for this division of labor is
! that an <literal>AFTER</> trigger can be certain it is seeing the final
! value of the row, while a <literal>BEFORE</> trigger cannot; there might
! be other <literal>BEFORE</> triggers firing after it. If you have no
! specific reason to make a trigger <literal>BEFORE</> or
! <literal>AFTER</>, the <literal>BEFORE</> case is more efficient, since
! the information about
the operation doesn't have to be saved until end of statement.
</para>
***************
*** 237,243 ****
Statement-level triggers follow simple visibility rules: none of
the changes made by a statement are visible to statement-level
triggers that are invoked before the statement, whereas all
! modifications are visible to statement-level after triggers.
</para>
</listitem>
--- 289,296 ----
Statement-level triggers follow simple visibility rules: none of
the changes made by a statement are visible to statement-level
triggers that are invoked before the statement, whereas all
! modifications are visible to statement-level <literal>AFTER</>
! triggers.
</para>
</listitem>
***************
*** 245,258 ****
<para>
The data change (insertion, update, or deletion) causing the
trigger to fire is naturally <emphasis>not</emphasis> visible
! to SQL commands executed in a row-level before trigger, because
! it hasn't happened yet.
</para>
</listitem>
<listitem>
<para>
! However, SQL commands executed in a row-level before
trigger <emphasis>will</emphasis> see the effects of data
changes for rows previously processed in the same outer
command. This requires caution, since the ordering of these
--- 298,311 ----
<para>
The data change (insertion, update, or deletion) causing the
trigger to fire is naturally <emphasis>not</emphasis> visible
! to SQL commands executed in a row-level <literal>BEFORE</> trigger,
! because it hasn't happened yet.
</para>
</listitem>
<listitem>
<para>
! However, SQL commands executed in a row-level <literal>BEFORE</>
trigger <emphasis>will</emphasis> see the effects of data
changes for rows previously processed in the same outer
command. This requires caution, since the ordering of these
***************
*** 263,269 ****
<listitem>
<para>
! When a row-level after trigger is fired, all data changes made
by the outer command are already complete, and are visible to
the invoked trigger function.
</para>
--- 316,323 ----
<listitem>
<para>
! When a row-level <literal>AFTER</> trigger is fired, all data
! changes made
by the outer command are already complete, and are visible to
the invoked trigger function.
</para>
***************
*** 376,381 ****
--- 430,444 ----
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>TRIGGER_FIRED_INSTEAD(tg_event)</literal></term>
+ <listitem>
+ <para>
+ Returns true if the trigger fired instead of the operation.
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><literal>TRIGGER_FIRED_AFTER(tg_event)</literal></term>
*** ./src/backend/catalog/index.c.orig 2010-08-14 23:52:07.000000000 +0100
--- ./src/backend/catalog/index.c 2010-08-15 09:53:21.000000000 +0100
***************
*** 825,830 ****
--- 825,831 ----
trigger->funcname = SystemFuncName("unique_key_recheck");
trigger->args = NIL;
trigger->before = false;
+ trigger->instead = false;
trigger->row = true;
trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE;
trigger->columns = NIL;
*** ./src/backend/catalog/information_schema.sql.orig 2010-08-28 16:44:27.000000000 +0100
--- ./src/backend/catalog/information_schema.sql 2010-08-31 12:43:19.000000000 +0100
***************
*** 1937,1943 ****
CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END
AS character_data) AS action_orientation,
CAST(
! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' ELSE 'AFTER' END
AS character_data) AS condition_timing,
CAST(null AS sql_identifier) AS condition_reference_old_table,
CAST(null AS sql_identifier) AS condition_reference_new_table,
--- 1937,1943 ----
CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END
AS character_data) AS action_orientation,
CAST(
! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' WHEN t.tgtype & 64 = 64 THEN 'INSTEAD OF' ELSE 'AFTER' END
AS character_data) AS condition_timing,
CAST(null AS sql_identifier) AS condition_reference_old_table,
CAST(null AS sql_identifier) AS condition_reference_new_table,
***************
*** 2233,2241 ****
THEN 'YES' ELSE 'NO' END
AS yes_or_no) AS is_insertable_into,
! CAST('NO' AS yes_or_no) AS is_trigger_updatable,
! CAST('NO' AS yes_or_no) AS is_trigger_deletable,
! CAST('NO' AS yes_or_no) AS is_trigger_insertable_into
FROM pg_namespace nc, pg_class c
--- 2233,2252 ----
THEN 'YES' ELSE 'NO' END
AS yes_or_no) AS is_insertable_into,
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 80 = 80)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_updatable,
!
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 72 = 72)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_deletable,
!
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 68 = 68)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_insertable_into
FROM pg_namespace nc, pg_class c
*** ./src/backend/catalog/sql_features.txt.orig 2010-09-04 15:10:12.000000000 +0100
--- ./src/backend/catalog/sql_features.txt 2010-09-04 15:10:42.000000000 +0100
***************
*** 426,432 ****
T211 Basic trigger capability 07 TRIGGER privilege YES
T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted
T212 Enhanced trigger capability YES
! T213 INSTEAD OF triggers NO
T231 Sensitive cursors YES
T241 START TRANSACTION statement YES
T251 SET TRANSACTION statement: LOCAL option NO
--- 426,432 ----
T211 Basic trigger capability 07 TRIGGER privilege YES
T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted
T212 Enhanced trigger capability YES
! T213 INSTEAD OF triggers YES
T231 Sensitive cursors YES
T241 START TRANSACTION statement YES
T251 SET TRANSACTION statement: LOCAL option NO
*** ./src/backend/commands/tablecmds.c.orig 2010-08-19 08:50:53.000000000 +0100
--- ./src/backend/commands/tablecmds.c 2010-08-19 08:50:52.000000000 +0100
***************
*** 5619,5624 ****
--- 5619,5625 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = myRel;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
/* Either ON INSERT or ON UPDATE */
***************
*** 5682,5687 ****
--- 5683,5689 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_DELETE;
fk_trigger->columns = NIL;
***************
*** 5735,5740 ****
--- 5737,5743 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_UPDATE;
fk_trigger->columns = NIL;
*** ./src/backend/commands/trigger.c.orig 2010-08-28 17:15:25.000000000 +0100
--- ./src/backend/commands/trigger.c 2010-09-01 16:58:12.000000000 +0100
***************
*** 150,159 ****
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 150,198 ----
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind == RELKIND_RELATION)
! {
! /* Forbid INSTEAD OF triggers on tables */
! if (stmt->instead)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a table",
! RelationGetRelationName(rel)),
! errhint("Define INSTEAD OF triggers on views")));
! }
! else if (rel->rd_rel->relkind == RELKIND_VIEW)
! {
! /* Only allow INSTEAD OF or STATEMENT-level triggers on views */
! if (!stmt->instead && stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a view",
! RelationGetRelationName(rel)),
! errhint("Define ROW-level BEFORE and AFTER triggers on tables")));
!
! /* Disallow STATEMENT-level INSTEAD OF triggers */
! if (stmt->instead && !stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("STATEMENT-level INSTEAD OF triggers are not supported"),
! errhint("Use FOR EACH ROW for INSTEAD OF triggers")));
!
! /* Disallow WHEN clauses with INSTEAD OF triggers */
! if (stmt->instead && stmt->whenClause)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a WHEN condition with an INSTEAD OF trigger")));
!
! /* Disallow column selection with INSTEAD OF triggers */
! if (stmt->instead && stmt->columns != NIL)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a column list with an INSTEAD OF trigger")));
! }
! else
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
***************
*** 188,193 ****
--- 227,234 ----
TRIGGER_CLEAR_TYPE(tgtype);
if (stmt->before)
TRIGGER_SETT_BEFORE(tgtype);
+ if (stmt->instead)
+ TRIGGER_SETT_INSTEAD(tgtype);
if (stmt->row)
TRIGGER_SETT_ROW(tgtype);
tgtype |= stmt->events;
***************
*** 198,203 ****
--- 239,252 ----
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("TRUNCATE FOR EACH ROW triggers are not supported")));
+ /* Disallow TRUNCATE triggers on VIEWs */
+ if (rel->rd_rel->relkind == RELKIND_VIEW && TRIGGER_FOR_TRUNCATE(tgtype))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is a view",
+ RelationGetRelationName(rel)),
+ errhint("Define TRUNCATE triggers on tables")));
+
/*
* Parse the WHEN clause, if any
*/
***************
*** 1031,1040 ****
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 1080,1090 ----
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION &&
! rel->rd_rel->relkind != RELKIND_VIEW)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
***************
*** 1506,1511 ****
--- 1556,1566 ----
n = trigdesc->n_before_row;
t = trigdesc->tg_before_row;
}
+ else if (TRIGGER_FOR_INSTEAD(trigger->tgtype))
+ {
+ n = trigdesc->n_instead_row;
+ t = trigdesc->tg_instead_row;
+ }
else
{
n = trigdesc->n_after_row;
***************
*** 1656,1661 ****
--- 1711,1729 ----
else
t[i] = NULL;
}
+ n = newdesc->n_instead_row;
+ t = newdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ {
+ if (n[i] > 0)
+ {
+ tnew = (int *) palloc(n[i] * sizeof(int));
+ memcpy(tnew, t[i], n[i] * sizeof(int));
+ t[i] = tnew;
+ }
+ else
+ t[i] = NULL;
+ }
n = newdesc->n_after_statement;
t = newdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
***************
*** 1698,1703 ****
--- 1766,1775 ----
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
pfree(t[i]);
+ t = trigdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ if (t[i] != NULL)
+ pfree(t[i]);
t = trigdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
***************
*** 2000,2005 ****
--- 2072,2121 ----
true, NULL, trigtuple, recheckIndexes, NULL);
}
+ HeapTuple
+ ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_INSERT];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_INSERT];
+ TriggerData LocTriggerData;
+ HeapTuple newtuple = trigtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, NULL, trigtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != trigtuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
{
***************
*** 2134,2139 ****
--- 2250,2297 ----
}
}
+ bool
+ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_DELETE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_DELETE];
+ TriggerData LocTriggerData;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, trigtuple, NULL))
+ continue;
+
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple == NULL)
+ return false; /* Delete was suppressed */
+ if (rettuple != trigtuple)
+ heap_freetuple(rettuple);
+ }
+ return true;
+ }
+
void
ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
{
***************
*** 2281,2286 ****
--- 2439,2488 ----
}
}
+ HeapTuple
+ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple oldtuple, HeapTuple newtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_UPDATE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_UPDATE];
+ TriggerData LocTriggerData;
+ HeapTuple intuple = newtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, oldtuple, newtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = oldtuple;
+ LocTriggerData.tg_newtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != intuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
{
*** ./src/backend/executor/execMain.c.orig 2010-08-15 00:04:31.000000000 +0100
--- ./src/backend/executor/execMain.c 2010-08-15 10:01:37.000000000 +0100
***************
*** 899,908 ****
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("cannot change view \"%s\"",
! RelationGetRelationName(resultRelationDesc))));
break;
default:
ereport(ERROR,
--- 899,905 ----
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! /* OK */
break;
default:
ereport(ERROR,
*** ./src/backend/executor/nodeModifyTable.c.orig 2010-08-19 08:53:10.000000000 +0100
--- ./src/backend/executor/nodeModifyTable.c 2010-09-04 22:53:39.000000000 +0100
***************
*** 149,154 ****
--- 149,200 ----
}
/* ----------------------------------------------------------------
+ * ExecCheckViewTriggers
+ *
+ * If the result relation is a view, check that it has the
+ * appropriate INSTEAD OF triggers to carry out the current
+ * operation.
+ * ----------------------------------------------------------------
+ */
+ static void
+ ExecCheckViewTriggers(ResultRelInfo *resultRelInfo,
+ CmdType operation)
+ {
+ TriggerDesc *trigDesc = resultRelInfo->ri_TrigDesc;
+
+ switch (operation)
+ {
+ case CMD_INSERT:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_INSERT] == 0)
+ ereport(ERROR,
+ (errmsg("cannot insert into a view"),
+ errhint("You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.")));
+ break;
+
+ case CMD_UPDATE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_UPDATE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot update a view"),
+ errhint("You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.")));
+ break;
+
+ case CMD_DELETE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_DELETE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot delete from a view"),
+ errhint("You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.")));
+ break;
+
+ default:
+ elog(ERROR, "unknown operation");
+ break;
+ }
+ }
+
+ /* ----------------------------------------------------------------
* ExecInsert
*
* For INSERT, we have to insert the tuple into the target relation
***************
*** 167,172 ****
--- 213,219 ----
Relation resultRelationDesc;
Oid newId;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* get the heap tuple out of the tuple table slot, making sure we have a
***************
*** 179,184 ****
--- 226,232 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
/*
* If the result relation has OIDs, force the tuple's OID to zero so that
***************
*** 195,202 ****
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /* BEFORE ROW INSERT Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
--- 243,251 ----
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /* BEFORE ROW INSERT Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
***************
*** 237,244 ****
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
(estate->es_processed)++;
estate->es_lastoid = newId;
--- 286,329 ----
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual insert
! */
! HeapTuple newtuple;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_INSERT);
! newtuple = ExecIRInsertTriggers(estate, resultRelInfo, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. 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(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! newId = InvalidOid;
! }
! else
! {
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
! }
(estate->es_processed)++;
estate->es_lastoid = newId;
***************
*** 251,258 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW INSERT Triggers */
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
--- 336,344 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW INSERT Triggers (tables only) */
! if (!resultRelIsView)
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
***************
*** 268,280 ****
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
--- 354,372 ----
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed.
! *
! * When deleting from a table, tupleid identifies the tuple to
! * 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.
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
***************
*** 284,298 ****
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW DELETE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
--- 376,393 ----
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
+ bool resultRelIsView;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /* BEFORE ROW DELETE Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
***************
*** 313,323 ****
* referential integrity updates in serializable transactions.
*/
ldelete:;
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 408,443 ----
* referential integrity updates in serializable transactions.
*/
ldelete:;
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual delete
! */
! HeapTupleData tuple;
! bool diddelete;
!
! tuple.t_data = oldtuple;
! tuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(tuple.t_self));
! tuple.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_DELETE);
! diddelete = ExecIRDeleteTriggers(estate, resultRelInfo, &tuple);
!
! if (!diddelete) /* trigger(s) did nothing */
! return NULL;
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
***************
*** 367,374 ****
* anyway, since the tuple is still visible to other transactions.
*/
! /* AFTER ROW DELETE Triggers */
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 487,495 ----
* anyway, since the tuple is still visible to other transactions.
*/
! /* AFTER ROW DELETE Triggers (tables only) */
! if (!resultRelIsView)
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
***************
*** 382,391 ****
HeapTupleData deltuple;
Buffer delbuffer;
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
--- 503,522 ----
HeapTupleData deltuple;
Buffer delbuffer;
! if (resultRelIsView)
! {
! deltuple.t_data = oldtuple;
! deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(deltuple.t_self));
! deltuple.t_tableOid = InvalidOid;
! }
! else
! {
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
! }
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
***************
*** 395,401 ****
slot, planSlot);
ExecClearTuple(slot);
! ReleaseBuffer(delbuffer);
return rslot;
}
--- 526,533 ----
slot, planSlot);
ExecClearTuple(slot);
! if (!resultRelIsView)
! ReleaseBuffer(delbuffer);
return rslot;
}
***************
*** 413,423 ****
--- 545,561 ----
* is, we don't want to get stuck in an infinite loop
* which corrupts your database..
*
+ * When updating a table, tupleid identifies the tuple to
+ * 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.
+ *
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecUpdate(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EPQState *epqstate,
***************
*** 430,435 ****
--- 568,574 ----
ItemPointerData update_ctid;
TransactionId update_xmax;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* abort the operation if not running transactions
***************
*** 448,456 ****
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW UPDATE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
--- 587,597 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /* BEFORE ROW UPDATE Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
***************
*** 501,511 ****
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in serializable transactions.
*/
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 642,696 ----
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in serializable transactions.
*/
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual update
! */
! HeapTupleData oldtup;
! HeapTuple newtuple;
!
! oldtup.t_data = oldtuple;
! oldtup.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(oldtup.t_self));
! oldtup.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_UPDATE);
! newtuple = ExecIRUpdateTriggers(estate, resultRelInfo,
! &oldtup, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. 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(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
***************
*** 568,576 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW UPDATE Triggers */
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
--- 753,762 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW UPDATE Triggers (tables only) */
! if (!resultRelIsView)
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
***************
*** 654,659 ****
--- 840,846 ----
TupleTableSlot *planSlot;
ItemPointer tupleid = NULL;
ItemPointerData tuple_ctid;
+ HeapTupleHeader oldtuple;
/*
* On first call, fire BEFORE STATEMENT triggers before proceeding.
***************
*** 713,735 ****
if (junkfilter != NULL)
{
/*
! * extract the 'ctid' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! /* shouldn't ever get a null result... */
! if (isNull)
! elog(ERROR, "ctid is NULL");
!
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
}
/*
--- 900,936 ----
if (junkfilter != NULL)
{
+ ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
+ Relation resultRel = resultRelInfo->ri_RelationDesc;
+
/*
! * extract the 'ctid' or 'wholerow' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "wholerow is NULL");
!
! oldtuple = DatumGetHeapTupleHeader(datum);
! }
! else
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "ctid is NULL");
!
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
! }
}
/*
***************
*** 745,755 ****
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, planSlot,
&node->mt_epqstate, estate);
break;
default:
--- 946,956 ----
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, oldtuple, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, oldtuple, planSlot,
&node->mt_epqstate, estate);
break;
default:
***************
*** 833,838 ****
--- 1034,1043 ----
i = 0;
foreach(l, node->plans)
{
+ Relation resultRel = estate->es_result_relation_info->ri_RelationDesc;
+ if (resultRel->rd_rel->relkind == RELKIND_VIEW)
+ ExecCheckViewTriggers(estate->es_result_relation_info, operation);
+
subplan = (Plan *) lfirst(l);
mtstate->mt_plans[i] = ExecInitNode(subplan, estate, eflags);
estate->es_result_relation_info++;
***************
*** 988,997 ****
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /* For UPDATE/DELETE, find the ctid junk attr now */
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
}
resultRelInfo->ri_junkFilter = j;
--- 1193,1216 ----
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /*
! * For UPDATE/DELETE of real tables, find the ctid junk
! * attr now. For VIEWs use the wholerow junk attr.
! */
! Relation resultRel = resultRelInfo->ri_RelationDesc;
!
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk wholerow column");
! }
! else
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
! }
}
resultRelInfo->ri_junkFilter = j;
*** ./src/backend/nodes/copyfuncs.c.orig 2010-08-19 08:55:28.000000000 +0100
--- ./src/backend/nodes/copyfuncs.c 2010-08-19 08:55:27.000000000 +0100
***************
*** 3229,3234 ****
--- 3229,3235 ----
COPY_NODE_FIELD(funcname);
COPY_NODE_FIELD(args);
COPY_SCALAR_FIELD(before);
+ COPY_SCALAR_FIELD(instead);
COPY_SCALAR_FIELD(row);
COPY_SCALAR_FIELD(events);
COPY_NODE_FIELD(columns);
*** ./src/backend/nodes/equalfuncs.c.orig 2010-08-19 08:56:51.000000000 +0100
--- ./src/backend/nodes/equalfuncs.c 2010-08-19 08:56:50.000000000 +0100
***************
*** 1686,1691 ****
--- 1686,1692 ----
COMPARE_NODE_FIELD(funcname);
COMPARE_NODE_FIELD(args);
COMPARE_SCALAR_FIELD(before);
+ COMPARE_SCALAR_FIELD(instead);
COMPARE_SCALAR_FIELD(row);
COMPARE_SCALAR_FIELD(events);
COMPARE_NODE_FIELD(columns);
*** ./src/backend/optimizer/plan/planmain.c.orig 2010-07-28 09:37:12.000000000 +0100
--- ./src/backend/optimizer/plan/planmain.c 2010-08-15 09:29:08.000000000 +0100
***************
*** 180,185 ****
--- 180,194 ----
add_base_rels_to_query(root, (Node *) parse->jointree);
/*
+ * If the query target is a VIEW, it won't be in the jointree, but we
+ * need a dummy RelOptInfo node for it. This need not have any stats in
+ * it because it always just goes at the top of the plan tree.
+ */
+ if (parse->resultRelation &&
+ root->simple_rel_array[parse->resultRelation] == NULL)
+ build_simple_rel(root, parse->resultRelation, RELOPT_OTHER_MEMBER_REL);
+
+ /*
* Examine the targetlist and qualifications, adding entries to baserel
* targetlists for all referenced Vars. Restrict and join clauses are
* added to appropriate lists belonging to the mentioned relations. We
*** ./src/backend/optimizer/prep/preptlist.c.orig 2010-07-28 09:37:12.000000000 +0100
--- ./src/backend/optimizer/prep/preptlist.c 2010-08-15 09:35:45.000000000 +0100
***************
*** 38,44 ****
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table);
/*
--- 38,44 ----
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel);
/*
***************
*** 52,57 ****
--- 52,58 ----
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
+ bool result_is_view = false;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
***************
*** 63,81 ****
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
}
/*
! * for heap_form_tuple to work, the targetlist must match the exact order
! * of the attributes. We also need to fill in any missing attributes. -ay
! * 10/94
*/
! if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
! tlist = expand_targetlist(tlist, command_type,
! result_relation, range_table);
/*
* for "update" and "delete" queries, add ctid of the result relation into
--- 64,100 ----
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
+ Relation rel;
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
+
+ /*
+ * Open the result relation. We assume that the rewriter already
+ * acquired at least AccessShareLock on it, so we need no lock here.
+ */
+ rel = heap_open(getrelid(result_relation, range_table), NoLock);
+ result_is_view = rel->rd_rel->relkind == RELKIND_VIEW;
+
+ /*
+ * for heap_form_tuple to work, the targetlist must match the exact
+ * order of the attributes. We also need to fill in any missing
+ * attributes. -ay 10/94
+ */
+ if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ tlist = expand_targetlist(tlist, command_type,
+ result_relation, rel);
+
+ heap_close(rel, NoLock);
}
/*
! * For an UPDATE, expand_targetlist already created a fresh tlist. For
! * DELETE, better do a listCopy so that we don't destructively modify
! * the original tlist (is this really necessary?).
*/
! if (command_type == CMD_DELETE)
! tlist = list_copy(tlist);
/*
* for "update" and "delete" queries, add ctid of the result relation into
***************
*** 83,90 ****
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
*/
! if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
{
TargetEntry *tle;
Var *var;
--- 102,114 ----
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
+ *
+ * We don't do this if the result relation is a view, since that won't
+ * expose a ctid. The rewriter should have already added a wholerow TLE
+ * for the view's subselect.
*/
! if (!result_is_view &&
! (command_type == CMD_UPDATE || command_type == CMD_DELETE))
{
TargetEntry *tle;
Var *var;
***************
*** 97,110 ****
pstrdup("ctid"),
true);
- /*
- * For an UPDATE, expand_targetlist already created a fresh tlist. For
- * DELETE, better do a listCopy so that we don't destructively modify
- * the original tlist (is this really necessary?).
- */
- if (command_type == CMD_DELETE)
- tlist = list_copy(tlist);
-
tlist = lappend(tlist, tle);
}
--- 121,126 ----
***************
*** 169,187 ****
}
else
{
! /* Not a table, so we need the whole row as a junk var */
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
}
}
--- 185,227 ----
}
else
{
! bool exists = false;
! ListCell *l;
!
! /*
! * Not a table, so we need the whole row as a junk var. If the
! * query target is a view, the rewriter will have already added
! * a whole row var, so don't add another for that RTE.
! */
! foreach(l, tlist)
! {
! TargetEntry *tle = (TargetEntry *) lfirst(l);
!
! if (tle->resjunk &&
! IsA(tle->expr, Var) &&
! ((Var *) tle->expr)->varno == rc->rti &&
! strcmp(tle->resname, "wholerow") == 0)
! {
! exists = true;
! break;
! }
! }
!
! if (!exists)
! {
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
! }
}
}
***************
*** 241,251 ****
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
- Relation rel;
int attrno,
numattrs;
--- 281,290 ----
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel)
{
List *new_tlist = NIL;
ListCell *tlist_item;
int attrno,
numattrs;
***************
*** 256,267 ****
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order. We assume
! * that the rewriter already acquired at least AccessShareLock on the
! * relation, so we need no lock here.
*/
- rel = heap_open(getrelid(result_relation, range_table), NoLock);
-
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
--- 295,302 ----
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order.
*/
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
***************
*** 399,406 ****
tlist_item = lnext(tlist_item);
}
- heap_close(rel, NoLock);
-
return new_tlist;
}
--- 434,439 ----
*** ./src/backend/optimizer/util/plancat.c.orig 2010-08-28 17:18:36.000000000 +0100
--- ./src/backend/optimizer/util/plancat.c 2010-08-28 17:18:35.000000000 +0100
***************
*** 441,447 ****
*tuples = 1;
break;
default:
! /* else it has no disk storage; probably shouldn't get here? */
*pages = 0;
*tuples = 0;
break;
--- 441,452 ----
*tuples = 1;
break;
default:
! /*
! * Else it has no disk storage; probably shouldn't get here,
! * unless this is a VIEW which is the query target, in which
! * case we don't care about the sizes, since it will always be
! * at the top of the plan tree.
! */
*pages = 0;
*tuples = 0;
break;
*** ./src/backend/parser/gram.y.orig 2010-08-28 17:26:46.000000000 +0100
--- ./src/backend/parser/gram.y 2010-08-28 17:26:44.000000000 +0100
***************
*** 245,251 ****
%type <str> OptSchemaName
%type <list> OptSchemaEltList
! %type <boolean> TriggerActionTime TriggerForSpec opt_trusted opt_restart_seqs
%type <list> TriggerEvents TriggerOneEvent
%type <value> TriggerFuncArg
--- 245,252 ----
%type <str> OptSchemaName
%type <list> OptSchemaEltList
! %type <boolean> TriggerForSpec opt_trusted opt_restart_seqs
! %type <ival> TriggerActionTime
%type <list> TriggerEvents TriggerOneEvent
%type <value> TriggerFuncArg
***************
*** 3377,3383 ****
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = $4;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
--- 3378,3385 ----
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = ($4 & 1) != 0;
! n->instead = ($4 & 2) != 0;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
***************
*** 3399,3404 ****
--- 3401,3407 ----
n->funcname = $17;
n->args = $19;
n->before = FALSE;
+ n->instead = FALSE;
n->row = TRUE;
n->events = intVal(linitial($6));
n->columns = (List *) lsecond($6);
***************
*** 3412,3419 ****
;
TriggerActionTime:
! BEFORE { $$ = TRUE; }
! | AFTER { $$ = FALSE; }
;
TriggerEvents:
--- 3415,3423 ----
;
TriggerActionTime:
! BEFORE { $$ = 1; }
! | AFTER { $$ = 0; }
! | INSTEAD OF { $$ = 2; }
;
TriggerEvents:
*** ./src/backend/rewrite/rewriteHandler.c.orig 2010-07-28 09:37:27.000000000 +0100
--- ./src/backend/rewrite/rewriteHandler.c 2010-08-15 09:07:20.000000000 +0100
***************
*** 564,570 ****
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
--- 564,572 ----
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions. For an UPDATE on a VIEW, add tlist entries assigning any
! * unassigned attributes their current values. The RHS of such assignments
! * will be rewritten to refer to the view's subselect node.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
***************
*** 724,729 ****
--- 726,753 ----
false);
}
+ /*
+ * For an UPDATE on a VIEW where the TLE is missing, assign the
+ * current value, which will be rewritten as a query from the view's
+ * subselect node by ApplyRetrieveRule().
+ */
+ if (new_tle == NULL && commandType == CMD_UPDATE &&
+ target_relation->rd_rel->relkind == RELKIND_VIEW)
+ {
+ Node *new_expr;
+
+ new_expr = (Node *) makeVar(parsetree->resultRelation,
+ attrno,
+ att_tup->atttypid,
+ att_tup->atttypmod,
+ 0);
+
+ new_tle = makeTargetEntry((Expr *) new_expr,
+ attrno,
+ pstrdup(NameStr(att_tup->attname)),
+ false);
+ }
+
if (new_tle)
new_tlist = lappend(new_tlist, new_tle);
}
***************
*** 1149,1154 ****
--- 1173,1180 ----
RangeTblEntry *rte,
*subrte;
RowMarkClause *rc;
+ Var *var;
+ TargetEntry *tle;
if (list_length(rule->actions) != 1)
elog(ERROR, "expected just one rule action");
***************
*** 1177,1220 ****
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! /*
! * VIEWs are really easy --- just plug the view query in as a subselect,
! * replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! /*
! * We move the view's permission check data down to its rangetable. The
! * checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
!
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as implicit
! * FOR UPDATE/SHARE, the same as the parser would have done if the view's
! * subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will be
! * made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
return parsetree;
}
--- 1203,1313 ----
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! if (rt_index == parsetree->resultRelation)
! {
! /*
! * We have a VIEW as the query target. For DELETE and UPDATE, we
! * add a new subselect RTE using the view's query and adjust any
! * VARs in the original query to point to this instead of the
! * original view RTE.
! *
! * We keep the original view in the rtable as the query target, and
! * any Vars in the returning list that reference it are left alone.
! *
! * The resulting jointree fromlist will not refer to the view RTE,
! * and so the planner won't try to join to it. This will result in
! * a plan with a ModifyTable node at the root, referring to the
! * original view relation, and a subselect based on the view's query
! * merged with any user conditions.
! *
! * For INSERTS we do nothing. The original view remains the query
! * target.
! */
! if (parsetree->commandType == CMD_DELETE ||
! parsetree->commandType == CMD_UPDATE)
! {
! List *returningList = parsetree->returningList;
! /*
! * Make a new subselect RTE from the view query and adjust the
! * the original query to point to this instead of the original
! * view, while preserving the view resultRelation and any
! * returningList Vars.
! */
! rte = copyObject(rt_fetch(rt_index, parsetree->rtable));
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! parsetree->rtable = lappend(parsetree->rtable, rte);
! parsetree->returningList = NIL;
!
! ChangeVarNodes((Node *) parsetree, rt_index,
! list_length(parsetree->rtable), 0);
!
! parsetree->resultRelation = rt_index;
! parsetree->returningList = returningList;
!
! /*
! * Add a "wholerow" junk TLE so that the executor can retrieve
! * the old VIEW tuples to pass to the INSTEAD OF triggers.
! */
! var = makeVar(list_length(parsetree->rtable),
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
!
! tle = makeTargetEntry((Expr *) var,
! list_length(parsetree->targetList) + 1,
! pstrdup("wholerow"),
! true);
! parsetree->targetList = lappend(parsetree->targetList, tle);
! }
! }
! else
! {
! /*
! * Selecting from the VIEW --- just plug the view query in as a
! * subselect, replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
!
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
!
! /*
! * We move the view's permission check data down to its rangetable.
! * The checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
!
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
!
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as
! * implicit FOR UPDATE/SHARE, the same as the parser would have done
! * if the view's subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will
! * be made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
! }
return parsetree;
}
*** ./src/backend/utils/adt/ruleutils.c.orig 2010-08-15 00:14:30.000000000 +0100
--- ./src/backend/utils/adt/ruleutils.c 2010-08-15 10:45:04.000000000 +0100
***************
*** 545,550 ****
--- 545,552 ----
if (TRIGGER_FOR_BEFORE(trigrec->tgtype))
appendStringInfo(&buf, "BEFORE");
+ else if (TRIGGER_FOR_INSTEAD(trigrec->tgtype))
+ appendStringInfo(&buf, "INSTEAD OF");
else
appendStringInfo(&buf, "AFTER");
if (TRIGGER_FOR_INSERT(trigrec->tgtype))
*** ./src/backend/utils/adt/tsvector_op.c.orig 2010-08-15 00:18:37.000000000 +0100
--- ./src/backend/utils/adt/tsvector_op.c 2010-08-15 12:50:47.000000000 +0100
***************
*** 1259,1265 ****
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
--- 1259,1265 ----
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
*** ./src/bin/pg_dump/pg_dump.c.orig 2010-08-15 00:21:49.000000000 +0100
--- ./src/bin/pg_dump/pg_dump.c 2010-08-15 10:52:30.000000000 +0100
***************
*** 11745,11750 ****
--- 11745,11752 ----
findx = 0;
if (TRIGGER_FOR_BEFORE(tginfo->tgtype))
appendPQExpBuffer(query, "BEFORE");
+ if (TRIGGER_FOR_INSTEAD(tginfo->tgtype))
+ appendPQExpBuffer(query, "INSTEAD OF");
else
appendPQExpBuffer(query, "AFTER");
if (TRIGGER_FOR_INSERT(tginfo->tgtype))
*** ./src/bin/psql/describe.c.orig 2010-08-03 17:16:15.000000000 +0100
--- ./src/bin/psql/describe.c 2010-08-15 10:50:29.000000000 +0100
***************
*** 1820,1827 ****
--- 1820,1838 ----
}
PQclear(result);
}
+ }
+ /*
+ * Print triggers next.
+ * This is part of the footer information about a table, but may also
+ * apply to a view.
+ */
+ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v')
+ {
/* print triggers (but only user-defined triggers) */
+ PGresult *result = NULL;
+ int tuples = 0;
+
if (tableinfo.hastriggers)
{
printfPQExpBuffer(&buf,
***************
*** 1935,1942 ****
--- 1946,1963 ----
}
PQclear(result);
}
+ }
+ /*
+ * Done printing trigger information for view or table.
+ * Finish printing the footer information about a table.
+ */
+ if (tableinfo.relkind == 'r')
+ {
/* print inherited tables */
+ PGresult *result = NULL;
+ int tuples = 0;
+
printfPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i WHERE c.oid=i.inhparent AND i.inhrelid = '%s' ORDER BY inhseqno", oid);
result = PSQLexec(buf.data, false);
*** ./src/include/catalog/pg_trigger.h.orig 2010-09-04 15:00:15.000000000 +0100
--- ./src/include/catalog/pg_trigger.h 2010-09-04 15:16:04.000000000 +0100
***************
*** 91,96 ****
--- 91,97 ----
#define TRIGGER_TYPE_DELETE (1 << 3)
#define TRIGGER_TYPE_UPDATE (1 << 4)
#define TRIGGER_TYPE_TRUNCATE (1 << 5)
+ #define TRIGGER_TYPE_INSTEAD (1 << 6)
/* Macros for manipulating tgtype */
#define TRIGGER_CLEAR_TYPE(type) ((type) = 0)
***************
*** 101,106 ****
--- 102,108 ----
#define TRIGGER_SETT_DELETE(type) ((type) |= TRIGGER_TYPE_DELETE)
#define TRIGGER_SETT_UPDATE(type) ((type) |= TRIGGER_TYPE_UPDATE)
#define TRIGGER_SETT_TRUNCATE(type) ((type) |= TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_SETT_INSTEAD(type) ((type) |= TRIGGER_TYPE_INSTEAD)
#define TRIGGER_FOR_ROW(type) ((type) & TRIGGER_TYPE_ROW)
#define TRIGGER_FOR_BEFORE(type) ((type) & TRIGGER_TYPE_BEFORE)
***************
*** 108,112 ****
--- 110,115 ----
#define TRIGGER_FOR_DELETE(type) ((type) & TRIGGER_TYPE_DELETE)
#define TRIGGER_FOR_UPDATE(type) ((type) & TRIGGER_TYPE_UPDATE)
#define TRIGGER_FOR_TRUNCATE(type) ((type) & TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_FOR_INSTEAD(type) ((type) & TRIGGER_TYPE_INSTEAD)
#endif /* PG_TRIGGER_H */
*** ./src/include/commands/trigger.h.orig 2010-08-15 00:24:40.000000000 +0100
--- ./src/include/commands/trigger.h 2010-09-01 16:59:34.000000000 +0100
***************
*** 53,58 ****
--- 53,59 ----
#define TRIGGER_EVENT_OPMASK 0x00000003
#define TRIGGER_EVENT_ROW 0x00000004
#define TRIGGER_EVENT_BEFORE 0x00000008
+ #define TRIGGER_EVENT_INSTEAD 0x00000040
/* More TriggerEvent flags, used only within trigger.c */
***************
*** 84,91 ****
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event))
/*
* Definitions for the replication role based firing.
--- 85,95 ----
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
+ #define TRIGGER_FIRED_INSTEAD(event) \
+ ((TriggerEvent) (event) & TRIGGER_EVENT_INSTEAD)
+
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event) && !TRIGGER_FIRED_INSTEAD (event))
/*
* Definitions for the replication role based firing.
***************
*** 135,140 ****
--- 139,147 ----
ResultRelInfo *relinfo,
HeapTuple trigtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRInsertTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSDeleteTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASDeleteTriggers(EState *estate,
***************
*** 146,151 ****
--- 153,161 ----
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid);
+ extern bool ExecIRDeleteTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSUpdateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASUpdateTriggers(EState *estate,
***************
*** 160,165 ****
--- 170,179 ----
ItemPointer tupleid,
HeapTuple newtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRUpdateTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple oldtuple,
+ HeapTuple newtuple);
extern void ExecBSTruncateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASTruncateTriggers(EState *estate,
*** ./src/include/nodes/parsenodes.h.orig 2010-08-19 08:59:46.000000000 +0100
--- ./src/include/nodes/parsenodes.h 2010-08-19 08:59:45.000000000 +0100
***************
*** 1608,1613 ****
--- 1608,1614 ----
List *funcname; /* qual. name of function to call */
List *args; /* list of (T_String) Values or NIL */
bool before; /* BEFORE/AFTER */
+ bool instead; /* INSTEAD OF (overrides BEFORE/AFTER) */
bool row; /* ROW/STATEMENT */
/* events uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */
int16 events; /* INSERT/UPDATE/DELETE/TRUNCATE */
*** ./src/include/utils/rel.h.orig 2010-08-15 00:30:33.000000000 +0100
--- ./src/include/utils/rel.h 2010-08-15 10:20:43.000000000 +0100
***************
*** 82,91 ****
--- 82,93 ----
uint16 n_before_statement[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_before_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ uint16 n_instead_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ int *tg_instead_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_statement[TRIGGER_NUM_EVENT_CLASSES];
/* The actual array of triggers is here */
*** ./src/pl/plperl/expected/plperl_trigger.out.orig 2010-09-04 14:58:26.000000000 +0100
--- ./src/pl/plperl/expected/plperl_trigger.out 2010-09-04 14:57:10.000000000 +0100
***************
*** 124,129 ****
--- 124,207 ----
CONTEXT: PL/Perl function "trigger_data"
DROP TRIGGER show_trigger_data_trig on trigger_test;
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'INSERT'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{new} = {'i' => '2', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ update trigger_test_view set v = 'update' where i = 1;
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'UPDATE'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{new} = {'i' => '1', 'v' => 'update'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ delete from trigger_test_view;
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'DELETE'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
DROP FUNCTION trigger_data();
CREATE OR REPLACE FUNCTION valid_id() RETURNS trigger AS $$
*** ./src/pl/plperl/plperl.c.orig 2010-07-31 12:19:30.000000000 +0100
--- ./src/pl/plperl/plperl.c 2010-08-30 23:39:05.000000000 +0100
***************
*** 954,959 ****
--- 954,961 ----
when = "BEFORE";
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
when = "AFTER";
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ when = "INSTEAD OF";
else
when = "UNKNOWN";
hv_store_string(hv, "when", newSVstring(when));
*** ./src/pl/plperl/sql/plperl_trigger.sql.orig 2010-09-04 10:57:46.000000000 +0100
--- ./src/pl/plperl/sql/plperl_trigger.sql 2010-09-04 11:13:40.000000000 +0100
***************
*** 60,65 ****
--- 60,79 ----
delete from trigger_test;
DROP TRIGGER show_trigger_data_trig on trigger_test;
+
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i = 1;
+ delete from trigger_test_view;
+
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
DROP FUNCTION trigger_data();
*** ./src/pl/plpgsql/src/pl_exec.c.orig 2010-08-28 17:29:02.000000000 +0100
--- ./src/pl/plpgsql/src/pl_exec.c 2010-08-30 23:37:49.000000000 +0100
***************
*** 581,588 ****
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE or AFTER");
var->isnull = false;
var->freeval = true;
--- 581,590 ----
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ var->value = CStringGetTextDatum("INSTEAD OF");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE, AFTER or INSTEAD OF");
var->isnull = false;
var->freeval = true;
*** ./src/pl/plpython/expected/plpython_trigger.out.orig 2010-09-04 11:22:40.000000000 +0100
--- ./src/pl/plpython/expected/plpython_trigger.out 2010-09-04 11:34:21.000000000 +0100
***************
*** 294,304 ****
CONTEXT: PL/Python function "trigger_data"
NOTICE: TD[when] => BEFORE
CONTEXT: PL/Python function "trigger_data"
DROP FUNCTION trigger_data() CASCADE;
! NOTICE: drop cascades to 3 other objects
! DETAIL: drop cascades to trigger show_trigger_data_trig_before on table trigger_test
! drop cascades to trigger show_trigger_data_trig_after on table trigger_test
! drop cascades to trigger show_trigger_data_trig_stmt on table trigger_test
--
-- trigger error handling
--
--- 294,374 ----
CONTEXT: PL/Python function "trigger_data"
NOTICE: TD[when] => BEFORE
CONTEXT: PL/Python function "trigger_data"
+ DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
+ DROP TRIGGER show_trigger_data_trig_before on trigger_test;
+ DROP TRIGGER show_trigger_data_trig_after on trigger_test;
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => INSERT
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => {'i': 2, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => None
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
+ update trigger_test_view set v = 'update' where i = 1;
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => UPDATE
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => {'i': 1, 'v': 'update'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
+ delete from trigger_test_view;
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => DELETE
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => None
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
DROP FUNCTION trigger_data() CASCADE;
! NOTICE: drop cascades to trigger show_trigger_data_trig on view trigger_test_view
! DROP VIEW trigger_test_view;
! delete from trigger_test;
--
-- trigger error handling
--
*** ./src/pl/plpython/plpython.c.orig 2010-08-28 17:30:36.000000000 +0100
--- ./src/pl/plpython/plpython.c 2010-08-30 23:39:52.000000000 +0100
***************
*** 846,851 ****
--- 846,853 ----
pltwhen = PyString_FromString("BEFORE");
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
pltwhen = PyString_FromString("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ pltwhen = PyString_FromString("INSTEAD OF");
else
{
elog(ERROR, "unrecognized WHEN tg_event: %u", tdata->tg_event);
*** ./src/pl/plpython/sql/plpython_trigger.sql.orig 2010-09-04 11:22:22.000000000 +0100
--- ./src/pl/plpython/sql/plpython_trigger.sql 2010-09-04 11:33:54.000000000 +0100
***************
*** 99,106 ****
delete from trigger_test;
truncate table trigger_test;
! DROP FUNCTION trigger_data() CASCADE;
--
-- trigger error handling
--- 99,122 ----
delete from trigger_test;
truncate table trigger_test;
! DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
! DROP TRIGGER show_trigger_data_trig_before on trigger_test;
! DROP TRIGGER show_trigger_data_trig_after on trigger_test;
!
! insert into trigger_test values(1,'insert');
! CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
!
! CREATE TRIGGER show_trigger_data_trig
! INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
! FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i = 1;
+ delete from trigger_test_view;
+
+ DROP FUNCTION trigger_data() CASCADE;
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
--
-- trigger error handling
*** ./src/pl/tcl/expected/pltcl_queries_1.out.orig 2010-09-04 11:45:57.000000000 +0100
--- ./src/pl/tcl/expected/pltcl_queries_1.out 2010-09-04 22:06:23.000000000 +0100
***************
*** 196,201 ****
--- 196,237 ----
NOTICE: TG_table_schema: public
NOTICE: TG_when: BEFORE
NOTICE: args: {23 skidoo}
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: NEW: {i: 2, v: insert}
+ NOTICE: OLD: {}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: INSERT
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ update trigger_test_view set v = 'update' where i=1;
+ NOTICE: NEW: {i: 1, v: update}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: UPDATE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ delete from trigger_test_view;
+ NOTICE: NEW: {}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: DELETE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
update trigger_test set v = 'update' where i = 1;
NOTICE: NEW: {i: 1, v: update}
NOTICE: OLD: {i: 1, v: insert}
*** ./src/pl/tcl/expected/pltcl_queries.out.orig 2010-09-04 11:38:26.000000000 +0100
--- ./src/pl/tcl/expected/pltcl_queries.out 2010-09-04 22:05:50.000000000 +0100
***************
*** 196,201 ****
--- 196,237 ----
NOTICE: TG_table_schema: public
NOTICE: TG_when: BEFORE
NOTICE: args: {23 skidoo}
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: NEW: {i: 2, v: insert}
+ NOTICE: OLD: {}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: INSERT
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ update trigger_test_view set v = 'update' where i=1;
+ NOTICE: NEW: {i: 1, v: update}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: UPDATE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ delete from trigger_test_view;
+ NOTICE: NEW: {}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: DELETE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
update trigger_test set v = 'update' where i = 1;
NOTICE: NEW: {i: 1, v: update}
NOTICE: OLD: {i: 1, v: insert}
*** ./src/pl/tcl/expected/pltcl_setup.out.orig 2010-09-04 11:37:59.000000000 +0100
--- ./src/pl/tcl/expected/pltcl_setup.out 2010-09-04 11:43:22.000000000 +0100
***************
*** 51,56 ****
--- 51,57 ----
-- dump trigger data
CREATE TABLE trigger_test
(i int, v text );
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
if { [info exists TG_relid] } {
***************
*** 85,90 ****
--- 86,94 ----
CREATE TRIGGER show_trigger_data_trig
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ CREATE TRIGGER show_trigger_data_view_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
--
-- Trigger function on every change to T_pkey1
--
*** ./src/pl/tcl/pltcl.c.orig 2010-07-31 12:26:53.000000000 +0100
--- ./src/pl/tcl/pltcl.c 2010-08-30 23:40:13.000000000 +0100
***************
*** 822,827 ****
--- 822,829 ----
Tcl_DStringAppendElement(&tcl_cmd, "BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
Tcl_DStringAppendElement(&tcl_cmd, "AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ Tcl_DStringAppendElement(&tcl_cmd, "INSTEAD OF");
else
elog(ERROR, "unrecognized WHEN tg_event: %u", trigdata->tg_event);
*** ./src/pl/tcl/sql/pltcl_queries.sql.orig 2010-09-04 11:37:44.000000000 +0100
--- ./src/pl/tcl/sql/pltcl_queries.sql 2010-09-04 22:01:02.000000000 +0100
***************
*** 76,80 ****
--- 76,85 ----
-- show dump of trigger data
insert into trigger_test values(1,'insert');
+
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i=1;
+ delete from trigger_test_view;
+
update trigger_test set v = 'update' where i = 1;
delete from trigger_test;
*** ./src/pl/tcl/sql/pltcl_setup.sql.orig 2010-09-04 11:37:22.000000000 +0100
--- ./src/pl/tcl/sql/pltcl_setup.sql 2010-09-04 11:42:59.000000000 +0100
***************
*** 60,65 ****
--- 60,67 ----
CREATE TABLE trigger_test
(i int, v text );
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
if { [info exists TG_relid] } {
***************
*** 96,101 ****
--- 98,106 ----
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ CREATE TRIGGER show_trigger_data_view_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
--
-- Trigger function on every change to T_pkey1
*** ./src/test/regress/expected/triggers.out.orig 2010-08-15 15:55:21.000000000 +0100
--- ./src/test/regress/expected/triggers.out 2010-08-30 23:50:32.000000000 +0100
***************
*** 791,793 ****
--- 791,1419 ----
DROP TABLE min_updates_test;
DROP TABLE min_updates_test_oids;
+ --
+ -- Test triggers on views
+ --
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ ERROR: cannot insert into a view
+ HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+ UPDATE main_view SET b = 20 WHERE a = 50;
+ ERROR: cannot update a view
+ HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+ DELETE FROM main_view WHERE a = 50;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a = 51;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- VIEW trigger function
+ CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
+ LANGUAGE plpgsql AS $$
+ declare
+ argstr text := '';
+ begin
+ 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 '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr;
+
+ if TG_WHEN = 'INSTEAD OF' then
+ if TG_OP = 'INSERT' then
+ raise NOTICE 'NEW: %', NEW;
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ raise NOTICE 'OLD: %, NEW: %', OLD, NEW;
+ UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ raise NOTICE 'OLD: %', OLD;
+ DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end if;
+
+ RETURN NULL;
+ end;
+ $$;
+ -- Before row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ -- After row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ -- Truncate triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ -- INSTEAD OF triggers aren't allowed on tables
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ -- Don't support WHEN clauses with INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: cannot use a WHEN condition with an INSTEAD OF trigger
+ -- Don't support column-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: cannot use a column list with an INSTEAD OF trigger
+ -- Don't support statement-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: STATEMENT-level INSTEAD OF triggers are not supported
+ HINT: Use FOR EACH ROW for INSTEAD OF triggers
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+ -- Valid BEFORE statement VIEW triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
+ CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt');
+ CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt');
+ -- Valid AFTER statement VIEW triggers
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt');
+ CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt');
+ CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt');
+ \set QUIET false
+ -- Insert into view using trigger
+ INSERT INTO main_view VALUES (20, 30);
+ NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt)
+ NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins)
+ NOTICE: NEW: (20,30)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt)
+ INSERT 0 1
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+ NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt)
+ NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins)
+ NOTICE: NEW: (21,31)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt)
+ a | b
+ ----+----
+ 21 | 31
+ (1 row)
+
+ INSERT 0 1
+ -- Table trigger will prevent updates
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (20,30), NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 0
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (21,31), NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ a | b
+ ---+---
+ (0 rows)
+
+ UPDATE 0
+ -- Remove table trigger to allow updates
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ DROP TRIGGER
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (20,30), NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 1
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (21,31), NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ a | b
+ ----+----
+ 21 | 32
+ (1 row)
+
+ UPDATE 1
+ -- Before and after stmt triggers should fire even when no rows are affected
+ UPDATE main_view SET b = 0 WHERE false;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 0
+ -- Delete from view using trigger
+ DELETE FROM main_view WHERE a IN (20,21);
+ NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (21,10)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (20,31)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (21,32)
+ NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt)
+ DELETE 3
+ DELETE FROM main_view WHERE a = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (31,10)
+ NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt)
+ a | b
+ ----+----
+ 31 | 10
+ (1 row)
+
+ DELETE 1
+ \set QUIET true
+ -- Describe view should list triggers
+ \d main_view
+ View "public.main_view"
+ Column | Type | Modifiers
+ --------+---------+-----------
+ a | integer |
+ b | integer |
+ Triggers:
+ after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt')
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt')
+ before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt')
+ before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt')
+ instead_of_delete_trig INSTEAD OF DELETE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del')
+ instead_of_insert_trig INSTEAD OF INSERT ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd')
+
+ -- Test dropping view triggers
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d+ main_view
+ View "public.main_view"
+ Column | Type | Modifiers | Storage | Description
+ --------+---------+-----------+---------+-------------
+ a | integer | | plain |
+ b | integer | | plain |
+ View definition:
+ SELECT main_table.a, main_table.b
+ FROM main_table;
+ Triggers:
+ after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt')
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt')
+ before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt')
+ before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd')
+
+ DROP VIEW main_view;
+ --
+ -- Test triggers on a join view
+ --
+ CREATE TABLE country_table (
+ country_id serial primary key,
+ country_name text unique not null,
+ continent text not null
+ );
+ NOTICE: CREATE TABLE will create implicit sequence "country_table_country_id_seq" for serial column "country_table.country_id"
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "country_table_pkey" for table "country_table"
+ NOTICE: CREATE TABLE / UNIQUE will create implicit index "country_table_country_name_key" for table "country_table"
+ INSERT INTO country_table (country_name, continent)
+ VALUES ('Japan', 'Asia'),
+ ('UK', 'Europe'),
+ ('USA', 'North America')
+ RETURNING *;
+ country_id | country_name | continent
+ ------------+--------------+---------------
+ 1 | Japan | Asia
+ 2 | UK | Europe
+ 3 | USA | North America
+ (3 rows)
+
+ CREATE TABLE city_table (
+ city_id serial primary key,
+ city_name text not null,
+ population bigint,
+ country_id int references country_table
+ );
+ NOTICE: CREATE TABLE will create implicit sequence "city_table_city_id_seq" for serial column "city_table.city_id"
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "city_table_pkey" for table "city_table"
+ CREATE VIEW city_view AS
+ SELECT city_id, city_name, population, country_name, continent
+ FROM city_table ci
+ LEFT JOIN country_table co ON co.country_id = ci.country_id;
+ CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS NOT NULL then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+ else
+ NEW.continent := NULL;
+ end if;
+
+ if NEW.city_id IS NOT NULL then
+ INSERT INTO city_table
+ VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id);
+ else
+ INSERT INTO city_table(city_name, population, country_id)
+ VALUES(NEW.city_name, NEW.population, ctry_id)
+ RETURNING city_id INTO NEW.city_id;
+ end if;
+
+ RETURN NEW;
+ end;
+ $$;
+ CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_insert();
+ CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ DELETE FROM city_table WHERE city_id = OLD.city_id;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end;
+ $$;
+ CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_delete();
+ CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS DISTINCT FROM OLD.country_name then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population,
+ country_id = ctry_id
+ WHERE city_id = OLD.city_id;
+ else
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population
+ WHERE city_id = OLD.city_id;
+ NEW.continent := OLD.continent;
+ end if;
+
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end;
+ $$;
+ CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_update();
+ -- INSERT .. RETURNING
+ \set QUIET false
+ INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 2 | London | 7556900 | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+---------------+------------+--------------+---------------
+ 3 | Washington DC | | USA | North America
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 123456 | New York | | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ INSERT 0 1
+ -- UPDATE .. RETURNING
+ UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error
+ ERROR: No such country: "Japon"
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match
+ UPDATE 0
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | | Japan | Asia
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | 13010279 | Japan | Asia
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 123456 | New York | | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+---------------
+ 123456 | New York | 8391881 | USA | North America
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2
+ WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *;
+ city_id | city_name | population | country_name | continent | city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------+---------+------------+------------+--------------+-----------
+ 2 | London | 7556900 | UK | Europe | 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ -- DELETE .. RETURNING
+ DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ DELETE 1
+ \set QUIET true
+ -- read-only view with WHERE clause
+ CREATE VIEW european_city_view AS
+ SELECT * FROM city_view WHERE continent = 'Europe';
+ SELECT count(*) FROM european_city_view;
+ count
+ -------
+ 1
+ (1 row)
+
+ CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql
+ AS 'begin RETURN NULL; end';
+ CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE
+ ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn();
+ \set QUIET false
+ INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z');
+ INSERT 0 0
+ UPDATE european_city_view SET population = 10000;
+ UPDATE 0
+ DELETE FROM european_city_view;
+ DELETE 0
+ \set QUIET true
+ -- rules bypassing no-op triggers
+ CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view
+ DO INSTEAD INSERT INTO city_view
+ VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent)
+ RETURNING *;
+ CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view
+ DO INSTEAD UPDATE city_view SET
+ city_name = NEW.city_name,
+ population = NEW.population,
+ country_name = NEW.country_name
+ WHERE city_id = OLD.city_id
+ RETURNING NEW.*;
+ CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view
+ DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *;
+ -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are
+ \set QUIET false
+ INSERT INTO european_city_view(city_name, country_name)
+ VALUES ('Cambridge', 'USA') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+---------------
+ 4 | Cambridge | | USA | North America
+ (1 row)
+
+ INSERT 0 1
+ UPDATE european_city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge';
+ UPDATE 0
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge';
+ DELETE 0
+ -- UPDATE and DELETE via rule and trigger
+ UPDATE city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE european_city_view SET population = 122800
+ WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | 122800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | 122800 | UK | Europe
+ (1 row)
+
+ DELETE 1
+ -- join UPDATE test
+ UPDATE city_view v SET population = 599657
+ FROM city_table ci, country_table co
+ WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA'
+ AND v.city_id = ci.city_id AND v.country_name = co.country_name
+ RETURNING co.country_id, v.country_name,
+ v.city_id, v.city_name, v.population;
+ country_id | country_name | city_id | city_name | population
+ ------------+--------------+---------+---------------+------------
+ 3 | USA | 3 | Washington DC | 599657
+ (1 row)
+
+ UPDATE 1
+ \set QUIET true
+ SELECT * FROM city_view;
+ city_id | city_name | population | country_name | continent
+ ---------+---------------+------------+--------------+---------------
+ 1 | Tokyo | 13010279 | Japan | Asia
+ 123456 | New York | 8391881 | USA | North America
+ 2 | London | 7556900 | UK | Europe
+ 3 | Washington DC | 599657 | USA | North America
+ (4 rows)
+
+ DROP TABLE city_table CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to view city_view
+ drop cascades to view european_city_view
+ DROP TABLE country_table;
*** ./src/test/regress/regress.c.orig 2010-07-31 12:28:32.000000000 +0100
--- ./src/test/regress/regress.c 2010-08-15 12:52:20.000000000 +0100
***************
*** 489,495 ****
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
--- 489,495 ----
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
*** ./src/test/regress/sql/triggers.sql.orig 2010-08-19 09:07:19.000000000 +0100
--- ./src/test/regress/sql/triggers.sql 2010-08-30 23:51:39.000000000 +0100
***************
*** 578,580 ****
--- 578,937 ----
DROP TABLE min_updates_test;
DROP TABLE min_updates_test_oids;
+
+ --
+ -- Test triggers on views
+ --
+
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ UPDATE main_view SET b = 20 WHERE a = 50;
+ DELETE FROM main_view WHERE a = 50;
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a = 51;
+
+ -- VIEW trigger function
+ CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
+ LANGUAGE plpgsql AS $$
+ declare
+ argstr text := '';
+ begin
+ 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 '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr;
+
+ if TG_WHEN = 'INSTEAD OF' then
+ if TG_OP = 'INSERT' then
+ raise NOTICE 'NEW: %', NEW;
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ raise NOTICE 'OLD: %, NEW: %', OLD, NEW;
+ UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ raise NOTICE 'OLD: %', OLD;
+ DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end if;
+
+ RETURN NULL;
+ end;
+ $$;
+
+ -- Before row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ -- After row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ -- Truncate triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ -- INSTEAD OF triggers aren't allowed on tables
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+
+ -- Don't support WHEN clauses with INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Don't support column-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Don't support statement-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+
+ -- Valid BEFORE statement VIEW triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
+
+ CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt');
+
+ CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt');
+
+ -- Valid AFTER statement VIEW triggers
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt');
+
+ CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt');
+
+ CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt');
+
+ \set QUIET false
+
+ -- Insert into view using trigger
+ INSERT INTO main_view VALUES (20, 30);
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+
+ -- Table trigger will prevent updates
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+
+ -- Remove table trigger to allow updates
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+
+ -- Before and after stmt triggers should fire even when no rows are affected
+ UPDATE main_view SET b = 0 WHERE false;
+
+ -- Delete from view using trigger
+ DELETE FROM main_view WHERE a IN (20,21);
+ DELETE FROM main_view WHERE a = 31 RETURNING a, b;
+
+ \set QUIET true
+
+ -- Describe view should list triggers
+ \d main_view
+
+ -- Test dropping view triggers
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d+ main_view
+ DROP VIEW main_view;
+
+ --
+ -- Test triggers on a join view
+ --
+ CREATE TABLE country_table (
+ country_id serial primary key,
+ country_name text unique not null,
+ continent text not null
+ );
+
+ INSERT INTO country_table (country_name, continent)
+ VALUES ('Japan', 'Asia'),
+ ('UK', 'Europe'),
+ ('USA', 'North America')
+ RETURNING *;
+
+ CREATE TABLE city_table (
+ city_id serial primary key,
+ city_name text not null,
+ population bigint,
+ country_id int references country_table
+ );
+
+ CREATE VIEW city_view AS
+ SELECT city_id, city_name, population, country_name, continent
+ FROM city_table ci
+ LEFT JOIN country_table co ON co.country_id = ci.country_id;
+
+ CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS NOT NULL then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+ else
+ NEW.continent := NULL;
+ end if;
+
+ if NEW.city_id IS NOT NULL then
+ INSERT INTO city_table
+ VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id);
+ else
+ INSERT INTO city_table(city_name, population, country_id)
+ VALUES(NEW.city_name, NEW.population, ctry_id)
+ RETURNING city_id INTO NEW.city_id;
+ end if;
+
+ RETURN NEW;
+ end;
+ $$;
+
+ CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_insert();
+
+ CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ DELETE FROM city_table WHERE city_id = OLD.city_id;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end;
+ $$;
+
+ CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_delete();
+
+ CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS DISTINCT FROM OLD.country_name then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population,
+ country_id = ctry_id
+ WHERE city_id = OLD.city_id;
+ else
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population
+ WHERE city_id = OLD.city_id;
+ NEW.continent := OLD.continent;
+ end if;
+
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end;
+ $$;
+
+ CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_update();
+
+ -- INSERT .. RETURNING
+ \set QUIET false
+
+ INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *;
+ INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *;
+ INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *;
+ INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *;
+ INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *;
+
+ -- UPDATE .. RETURNING
+ UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK
+
+ UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *;
+ UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *;
+ UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *;
+ UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *;
+ UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2
+ WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *;
+
+ -- DELETE .. RETURNING
+ DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *;
+
+ \set QUIET true
+
+ -- read-only view with WHERE clause
+ CREATE VIEW european_city_view AS
+ SELECT * FROM city_view WHERE continent = 'Europe';
+ SELECT count(*) FROM european_city_view;
+
+ CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql
+ AS 'begin RETURN NULL; end';
+
+ CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE
+ ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn();
+
+ \set QUIET false
+
+ INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z');
+ UPDATE european_city_view SET population = 10000;
+ DELETE FROM european_city_view;
+
+ \set QUIET true
+
+ -- rules bypassing no-op triggers
+ CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view
+ DO INSTEAD INSERT INTO city_view
+ VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent)
+ RETURNING *;
+
+ CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view
+ DO INSTEAD UPDATE city_view SET
+ city_name = NEW.city_name,
+ population = NEW.population,
+ country_name = NEW.country_name
+ WHERE city_id = OLD.city_id
+ RETURNING NEW.*;
+
+ CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view
+ DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *;
+
+ -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are
+ \set QUIET false
+
+ INSERT INTO european_city_view(city_name, country_name)
+ VALUES ('Cambridge', 'USA') RETURNING *;
+ UPDATE european_city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge';
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge';
+
+ -- UPDATE and DELETE via rule and trigger
+ UPDATE city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge' RETURNING *;
+ UPDATE european_city_view SET population = 122800
+ WHERE city_name = 'Cambridge' RETURNING *;
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *;
+
+ -- join UPDATE test
+ UPDATE city_view v SET population = 599657
+ FROM city_table ci, country_table co
+ WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA'
+ AND v.city_id = ci.city_id AND v.country_name = co.country_name
+ RETURNING co.country_id, v.country_name,
+ v.city_id, v.city_name, v.population;
+
+ \set QUIET true
+
+ SELECT * FROM city_view;
+
+ DROP TABLE city_table CASCADE;
+ DROP TABLE country_table;
--On 5. September 2010 09:09:55 +0100 Dean Rasheed
<dean.a.rasheed@gmail.com> wrote:
I had a first look on your patch, great work!
Attached is an updated patch with more tests and docs, and a few minor
code tidy ups. I think that the INSTEAD OF triggers part of the patch
is compliant with Feature T213 of the SQL 2008 standard. As discussed,
Reading the past discussions, there was some mention about the RETURNING
clause.
I see Oracle doesn't allow its RETURNING INTO clause with INSTEAD OF
triggers (at least my 10g XE instance here doesn't allow it, not sure about
newer versions). I assume the following example might have some surprising
effects on users:
CREATE TABLE foo(id integer);
CREATE VIEW vfoo AS SELECT 'bernd', * FROM foo;
CREATE OR REPLACE FUNCTION insert_foo() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN INSERT INTO foo VALUES(NEW.id);
RETURN NEW;
END; $$;
CREATE TRIGGER insert_vfoo INSTEAD OF INSERT ON vfoo
FOR EACH ROW EXECUTE PROCEDURE insert_foo();
INSERT INTO vfoo VALUES('helmle', 2) RETURNING *;
text | id
--------+----
helmle | 2
(1 row)
SELECT * FROM vfoo;
text | id
-------+----
bernd | 2
(1 row)
This is solvable by a properly designed trigger function, but maybe we need
to do something about this?
I don't plan to add the syntax to allow triggers on views to be
disabled at this time, but that should be easy to implement, if there
is a use case for it.
I really don't see a need for this at the moment. We don't have DISABLE
RULE either. I'm going to post some additional comments once i've
understand all things.
--
Thanks
Bernd
On 2010-09-23 1:16 AM, Bernd Helmle wrote:
INSERT INTO vfoo VALUES('helmle', 2) RETURNING *;
text | id
--------+----
helmle | 2
(1 row)SELECT * FROM vfoo;
text | id
-------+----
bernd | 2
(1 row)This is solvable by a properly designed trigger function, but maybe we need
to do something about this?
I really don't think we should limit what people are allowed to do in
the trigger function.
Besides, even if the RETURNING clause returned 'bernd' in the above
case, I think it would be even *more* surprising. The trigger function
explicitly returns NEW which has 'helmle' as the first field.
Regards,
Marko Tiikkaja
On 23 September 2010 00:26, Marko Tiikkaja
<marko.tiikkaja@cs.helsinki.fi> wrote:
On 2010-09-23 1:16 AM, Bernd Helmle wrote:
INSERT INTO vfoo VALUES('helmle', 2) RETURNING *;
text | id
--------+----
helmle | 2
(1 row)SELECT * FROM vfoo;
text | id
-------+----
bernd | 2
(1 row)This is solvable by a properly designed trigger function, but maybe we
need
to do something about this?I really don't think we should limit what people are allowed to do in the
trigger function.Besides, even if the RETURNING clause returned 'bernd' in the above case, I
think it would be even *more* surprising. The trigger function explicitly
returns NEW which has 'helmle' as the first field.
Yes, I agree. To me this is the least surprising behaviour. I think a
more common case would be where the trigger computed a value (such as
the 'last updated' example). The executor doesn't have any kind of a
handle on the row inserted by the trigger, so it has to rely on the
function return value to support RETURNING.
I can confirm the latest Oracle (11g R2 Enterprise Edition) does not
support RETURNING INTO with INSTEAD OF triggers (although it does work
with its auto-updatable views), presumably because it's triggers don't
return values, but I think it would be a shame for us to not support
it.
Regards,
Dean
--On 23. September 2010 08:59:32 +0100 Dean Rasheed
<dean.a.rasheed@gmail.com> wrote:
Yes, I agree. To me this is the least surprising behaviour. I think a
more common case would be where the trigger computed a value (such as
the 'last updated' example). The executor doesn't have any kind of a
handle on the row inserted by the trigger, so it has to rely on the
function return value to support RETURNING.
I didn't mean to forbid it altogether, but at least to document
explicitely, that the trigger returns a VIEW's NEW tuple, not the one of
the base table (and may modify it). But you've already adressed this in
your doc patches, so nothing to worry about further.
--
Thanks
Bernd
--On 8. September 2010 09:00:33 +0100 Dean Rasheed
<dean.a.rasheed@gmail.com> wrote:
Here's an updated version with improved formatting and a few minor
wording changes to the triggers chapter.
This version doesn't apply clean anymore due to some rejects in
plainmain.c. Corrected version attached.
--
Thanks
Bernd
Attachments:
view_trigger2.patchapplication/octet-stream; name=view_trigger2.patchDownload
diff --git a/contrib/spi/autoinc.c b/contrib/spi/autoinc.c
index 4552fc3..a3373e6 100644
*** a/contrib/spi/autoinc.c
--- b/contrib/spi/autoinc.c
*************** autoinc(PG_FUNCTION_ARGS)
*** 38,44 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
--- 38,44 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
diff --git a/contrib/spi/insert_username.c b/contrib/spi/insert_username.c
index da6764f..3af8c03 100644
*** a/contrib/spi/insert_username.c
--- b/contrib/spi/insert_username.c
*************** insert_username(PG_FUNCTION_ARGS)
*** 41,47 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
--- 41,47 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
diff --git a/contrib/spi/moddatetime.c b/contrib/spi/moddatetime.c
index e90c0b5..3a2fdf7 100644
*** a/contrib/spi/moddatetime.c
--- b/contrib/spi/moddatetime.c
*************** moddatetime(PG_FUNCTION_ARGS)
*** 47,53 ****
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
--- 47,53 ----
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
diff --git a/contrib/spi/timetravel.c b/contrib/spi/timetravel.c
index 8bae313..e215670 100644
*** a/contrib/spi/timetravel.c
--- b/contrib/spi/timetravel.c
*************** timetravel(PG_FUNCTION_ARGS)
*** 122,128 ****
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
--- 122,128 ----
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 8e4081c..b7da10b 100644
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 4795,4801 ****
</indexterm>
<para>
! The catalog <structname>pg_trigger</structname> stores triggers on tables.
See <xref linkend="sql-createtrigger">
for more information.
</para>
--- 4795,4801 ----
</indexterm>
<para>
! The catalog <structname>pg_trigger</structname> stores triggers on tables and views.
See <xref linkend="sql-createtrigger">
for more information.
</para>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 509efea..7c039a1 100644
*** a/doc/src/sgml/information_schema.sgml
--- b/doc/src/sgml/information_schema.sgml
*************** ORDER BY c.ordinal_position;
*** 4885,4892 ****
<para>
The view <literal>triggers</literal> contains all triggers defined
! in the current database on tables that the current user owns or has
! some non-SELECT privilege on.
</para>
<table>
--- 4885,4892 ----
<para>
The view <literal>triggers</literal> contains all triggers defined
! in the current database on tables and views that the current user owns
! or has some non-SELECT privilege on.
</para>
<table>
*************** ORDER BY c.ordinal_position;
*** 4990,4997 ****
<entry><literal>condition_timing</literal></entry>
<entry><type>character_data</type></entry>
<entry>
! Time at which the trigger fires (<literal>BEFORE</literal> or
! <literal>AFTER</literal>)
</entry>
</row>
--- 4990,4997 ----
<entry><literal>condition_timing</literal></entry>
<entry><type>character_data</type></entry>
<entry>
! Time at which the trigger fires (<literal>BEFORE</literal>,
! <literal>AFTER</literal> or <literal>INSTEAD OF</literal>)
</entry>
</row>
*************** ORDER BY c.ordinal_position;
*** 5562,5580 ****
<row>
<entry><literal>is_trigger_updatable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>Applies to a feature not available in <productname>PostgreSQL</></entry>
</row>
<row>
<entry><literal>is_trigger_deletable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>Applies to a feature not available in <productname>PostgreSQL</></entry>
</row>
<row>
<entry><literal>is_trigger_insertable_into</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>Applies to a feature not available in <productname>PostgreSQL</></entry>
</row>
</tbody>
</tgroup>
--- 5562,5589 ----
<row>
<entry><literal>is_trigger_updatable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>
! <literal>YES</> if the view has an <literal>INSTEAD OF</>
! <command>UPDATE</> trigger defined on it, <literal>NO</> if not
! </entry>
</row>
<row>
<entry><literal>is_trigger_deletable</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>
! <literal>YES</> if the view has an <literal>INSTEAD OF</>
! <command>DELETE</> trigger defined on it, <literal>NO</> if not
! </entry>
</row>
<row>
<entry><literal>is_trigger_insertable_into</literal></entry>
<entry><type>yes_or_no</type></entry>
! <entry>
! <literal>YES</> if the view has an <literal>INSTEAD OF</>
! <command>INSERT</> trigger defined on it, <literal>NO</> if not
! </entry>
</row>
</tbody>
</tgroup>
diff --git a/doc/src/sgml/plperl.sgml b/doc/src/sgml/plperl.sgml
index 864b53d..4dee321 100644
*** a/doc/src/sgml/plperl.sgml
--- b/doc/src/sgml/plperl.sgml
*************** $$ LANGUAGE plperl;
*** 980,986 ****
<term><literal>$_TD->{when}</literal></term>
<listitem>
<para>
! When the trigger was called: <literal>BEFORE</literal>, <literal>AFTER</literal>, or <literal>UNKNOWN</literal>
</para>
</listitem>
</varlistentry>
--- 980,988 ----
<term><literal>$_TD->{when}</literal></term>
<listitem>
<para>
! When the trigger was called: <literal>BEFORE</literal>,
! <literal>AFTER</literal>, <literal>INSTEAD OF</literal> or
! <literal>UNKNOWN</literal>
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index d3bf847..4bbec80 100644
*** a/doc/src/sgml/plpgsql.sgml
--- b/doc/src/sgml/plpgsql.sgml
*************** RAISE unique_violation USING MESSAGE = '
*** 3112,3120 ****
<term><varname>TG_WHEN</varname></term>
<listitem>
<para>
! Data type <type>text</type>; a string of either
! <literal>BEFORE</literal> or <literal>AFTER</literal>
! depending on the trigger's definition.
</para>
</listitem>
</varlistentry>
--- 3112,3120 ----
<term><varname>TG_WHEN</varname></term>
<listitem>
<para>
! Data type <type>text</type>; a string of
! <literal>BEFORE</literal>, <literal>AFTER</literal> or
! <literal>INSTEAD OF</literal> depending on the trigger's definition.
</para>
</listitem>
</varlistentry>
*************** RAISE unique_violation USING MESSAGE = '
*** 3239,3244 ****
--- 3239,3259 ----
</para>
<para>
+ <literal>INSTEAD OF</> triggers (which are always row-level triggers,
+ and may only be used on views) can return null to signal that they did
+ not perform any updates, and that the rest of the operation for this
+ row should be skipped (i.e., subsequent triggers are not fired, and the
+ <command>INSERT</>/<command>UPDATE</>/<command>DELETE</> did not occur
+ for this row). Otherwise a nonnull value should be returned, to signal
+ that the trigger performed the requested operation. For
+ <command>INSERT</> and <command>UPDATE</> operations, the return value
+ should be <varname>NEW</>, which the trigger function may modify to
+ support <command>INSERT RETURNING</> and <command>UPDATE RETURNING</>.
+ For <command>DELETE</> operations, the return value should be
+ <varname>OLD</>.
+ </para>
+
+ <para>
The return value of a row-level trigger
fired <literal>AFTER</literal> or a statement-level trigger
fired <literal>BEFORE</> or <literal>AFTER</> is
*************** AFTER INSERT OR UPDATE OR DELETE ON emp
*** 3355,3360 ****
--- 3370,3454 ----
</example>
<para>
+ A variation of the previous example uses a view joining the main table
+ to the audit table, to show when each entry was last modified. This
+ approach still records the full audit trail of changes to the table,
+ but also presents a simplified view of the audit trail, showing just
+ the last modified timestamp derived from the audit trail for each entry.
+ <xref linkend="plpgsql-view-trigger-audit-example"> shows an example
+ of an audit trigger on a view in <application>PL/pgSQL</application>.
+ </para>
+
+ <example id="plpgsql-view-trigger-audit-example">
+ <title>A <application>PL/pgSQL</application> View Trigger Procedure For Auditing</title>
+
+ <para>
+ This example uses a trigger on the view to make it updatable, and
+ ensure that any insert, update or delete of a row in the view is
+ recorded (i.e., audited) in the emp_audit table. The current time
+ and user name are recorded, together with the type of operation
+ performed, and the view displays the last modified time of each row.
+ </para>
+
+ <programlisting>
+ CREATE TABLE emp (
+ empname text PRIMARY KEY,
+ salary integer
+ );
+
+ CREATE TABLE emp_audit(
+ operation char(1) NOT NULL,
+ userid text NOT NULL,
+ empname text NOT NULL,
+ salary integer,
+ stamp timestamp NOT NULL
+ );
+
+ CREATE VIEW emp_view AS
+ SELECT e.empname,
+ e.salary,
+ max(ea.stamp) AS last_updated
+ FROM emp e
+ LEFT JOIN emp_audit ea ON ea.empname = e.empname
+ GROUP BY 1, 2;
+
+ CREATE OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$
+ BEGIN
+ --
+ -- Perform the required operation on emp, and create a row in emp_audit
+ -- to reflect the change made to emp.
+ --
+ IF (TG_OP = 'DELETE') THEN
+ DELETE FROM emp WHERE empname = OLD.empname;
+ IF NOT FOUND THEN RETURN NULL; END IF;
+
+ OLD.last_updated = now();
+ INSERT INTO emp_audit VALUES('D', user, OLD.*);
+ RETURN OLD;
+ ELSIF (TG_OP = 'UPDATE') THEN
+ UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;
+ IF NOT FOUND THEN RETURN NULL; END IF;
+
+ NEW.last_updated = now();
+ INSERT INTO emp_audit VALUES('U', user, NEW.*);
+ RETURN NEW;
+ ELSIF (TG_OP = 'INSERT') THEN
+ INSERT INTO emp VALUES(NEW.empname, NEW.salary);
+
+ NEW.last_updated = now();
+ INSERT INTO emp_audit VALUES('I', user, NEW.*);
+ RETURN NEW;
+ END IF;
+ END;
+ $$ LANGUAGE plpgsql;
+
+ CREATE TRIGGER emp_audit
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
+ FOR EACH ROW EXECUTE PROCEDURE update_emp_view();
+ </programlisting>
+ </example>
+
+ <para>
One use of triggers is to maintain a summary table
of another table. The resulting summary can be used in place of the
original table for certain queries — often with vastly reduced run
diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml
index c544563..1774b2b 100644
*** a/doc/src/sgml/plpython.sgml
--- b/doc/src/sgml/plpython.sgml
*************** $$ LANGUAGE plpythonu;
*** 762,768 ****
<listitem>
<para>
contains one of <literal>BEFORE</>, <literal>AFTER</>,
! or <literal>UNKNOWN</>.
</para>
</listitem>
</varlistentry>
--- 762,768 ----
<listitem>
<para>
contains one of <literal>BEFORE</>, <literal>AFTER</>,
! <literal>INSTEAD OF</> or <literal>UNKNOWN</>.
</para>
</listitem>
</varlistentry>
*************** $$ LANGUAGE plpythonu;
*** 838,849 ****
</para>
<para>
! If <literal>TD["when"]</literal> is <literal>BEFORE</> and
<literal>TD["level"]</literal> is <literal>ROW</>, you can
return <literal>None</literal> or <literal>"OK"</literal> from the
Python function to indicate the row is unmodified,
! <literal>"SKIP"</> to abort the event, or <literal>"MODIFY"</> to
! indicate you've modified the row.
Otherwise the return value is ignored.
</para>
</sect1>
--- 838,851 ----
</para>
<para>
! If <literal>TD["when"]</literal> is <literal>BEFORE</> or
! <literal>INSTEAD OF</> and
<literal>TD["level"]</literal> is <literal>ROW</>, you can
return <literal>None</literal> or <literal>"OK"</literal> from the
Python function to indicate the row is unmodified,
! <literal>"SKIP"</> to abort the event, or if <literal>TD["event"]</>
! is <command>INSERT</> or <command>UPDATE</> you can return
! <literal>"MODIFY"</> to indicate you've modified the new row.
Otherwise the return value is ignored.
</para>
</sect1>
diff --git a/doc/src/sgml/pltcl.sgml b/doc/src/sgml/pltcl.sgml
index eb29a8f..833ee1f 100644
*** a/doc/src/sgml/pltcl.sgml
--- b/doc/src/sgml/pltcl.sgml
*************** SELECT 'doesn''t' AS ret
*** 567,574 ****
<term><varname>$TG_when</varname></term>
<listitem>
<para>
! The string <literal>BEFORE</> or <literal>AFTER</> depending on the
! type of trigger event.
</para>
</listitem>
</varlistentry>
--- 567,574 ----
<term><varname>$TG_when</varname></term>
<listitem>
<para>
! The string <literal>BEFORE</>, <literal>AFTER</> or
! <literal>INSTEAD OF</> depending on the type of trigger event.
</para>
</listitem>
</varlistentry>
*************** SELECT 'doesn''t' AS ret
*** 641,650 ****
the operation (<command>INSERT</>/<command>UPDATE</>/<command>DELETE</>) that fired the trigger will proceed
normally. <literal>SKIP</> tells the trigger manager to silently suppress
the operation for this row. If a list is returned, it tells PL/Tcl to
! return a modified row to the trigger manager that will be inserted
! instead of the one given in <varname>$NEW</>. (This works for <command>INSERT</> and <command>UPDATE</>
! only.) Needless to say that all this is only meaningful when the trigger
! is <literal>BEFORE</> and <command>FOR EACH ROW</>; otherwise the return value is ignored.
</para>
<para>
--- 641,654 ----
the operation (<command>INSERT</>/<command>UPDATE</>/<command>DELETE</>) that fired the trigger will proceed
normally. <literal>SKIP</> tells the trigger manager to silently suppress
the operation for this row. If a list is returned, it tells PL/Tcl to
! return a modified row to the trigger manager. This is only meaningful
! for row-level <literal>BEFORE</> <command>INSERT</> or <command>UPDATE</>
! triggers for which the modified row will be inserted instead of the one
! given in <varname>$NEW</>; or for row-level <literal>INSTEAD OF</>
! <command>INSERT</> or <command>UPDATE</> triggers where the returned row
! is used to support <command>INSERT RETURNING</> and
! <command>UPDATE RETURNING</> commands. The return value is ignored for
! other types of triggers.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_rule.sgml b/doc/src/sgml/ref/create_rule.sgml
index 5d2182c..d70d1f4 100644
*** a/doc/src/sgml/ref/create_rule.sgml
--- b/doc/src/sgml/ref/create_rule.sgml
*************** CREATE [ OR REPLACE ] RULE <replaceable
*** 73,79 ****
sufficient for your purposes) to replace update actions on the view
with appropriate updates on other tables. If you want to support
<command>INSERT RETURNING</> and so on, then be sure to put a suitable
! <literal>RETURNING</> clause into each of these rules.
</para>
<para>
--- 73,81 ----
sufficient for your purposes) to replace update actions on the view
with appropriate updates on other tables. If you want to support
<command>INSERT RETURNING</> and so on, then be sure to put a suitable
! <literal>RETURNING</> clause into each of these rules. Alternatively,
! an updatable view may be implemented using <literal>INSTEAD OF</>
! triggers (see <xref linkend="sql-createtrigger">).
</para>
<para>
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index 1934113..9b26d63 100644
*** a/doc/src/sgml/ref/create_trigger.sgml
--- b/doc/src/sgml/ref/create_trigger.sgml
*************** PostgreSQL documentation
*** 21,27 ****
<refsynopsisdiv>
<synopsis>
! CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTER } { <replaceable class="PARAMETER">event</replaceable> [ OR ... ] }
ON <replaceable class="PARAMETER">table</replaceable> [ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( <replaceable class="parameter">condition</replaceable> ) ]
EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
--- 21,27 ----
<refsynopsisdiv>
<synopsis>
! CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTER | INSTEAD OF } { <replaceable class="PARAMETER">event</replaceable> [ OR ... ] }
ON <replaceable class="PARAMETER">table</replaceable> [ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( <replaceable class="parameter">condition</replaceable> ) ]
EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
*************** CREATE TRIGGER <replaceable class="PARAM
*** 33,53 ****
<para>
<command>CREATE TRIGGER</command> creates a new trigger. The
! trigger will be associated with the specified table and will
execute the specified function <replaceable
class="parameter">function_name</replaceable> when certain events occur.
</para>
<para>
! The trigger can be specified to fire either before the
operation is attempted on a row (before constraints are checked and
the <command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> is attempted) or after the operation has
completed (after constraints are checked and the
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> has completed). If the trigger fires
! before the event, the trigger can skip the operation for the
! current row, or change the row being inserted (for
<command>INSERT</command> and <command>UPDATE</command> operations
only). If the trigger fires after the event, all changes, including
the effects of other triggers, are <quote>visible</quote>
--- 33,54 ----
<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.
</para>
<para>
! The trigger can be specified to fire before the
operation is attempted on a row (before constraints are checked and
the <command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> is attempted); after the operation has
completed (after constraints are checked and the
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command> has completed); or instead of the operation
! (in the case of inserts, updates or deletes on a view).
! If the trigger fires before or instead of the event, the trigger can skip
! the operation for the current row, or change the row being inserted (for
<command>INSERT</command> and <command>UPDATE</command> operations
only). If the trigger fires after the event, all changes, including
the effects of other triggers, are <quote>visible</quote>
*************** CREATE TRIGGER <replaceable class="PARAM
*** 68,86 ****
</para>
<para>
In addition, triggers may be defined to fire for a
! <command>TRUNCATE</command>, though only
! <literal>FOR EACH STATEMENT</literal>.
</para>
<para>
! Also, a trigger definition can specify a Boolean <literal>WHEN</>
condition, which will be tested to see whether the trigger should
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
is not so useful for them since the condition cannot refer to any values
! in the table.
</para>
<para>
--- 69,144 ----
</para>
<para>
+ Triggers that are specified to fire <literal>INSTEAD OF</> the trigger
+ event must be marked <literal>FOR EACH ROW</>, and can only be defined
+ on views. <literal>BEFORE</> and <literal>AFTER</> triggers on a view
+ must be marked as <literal>FOR EACH STATEMENT</>.
+ </para>
+
+ <para>
In addition, triggers may be defined to fire for a
! <command>TRUNCATE</command> (tables only), though only
! <literal>FOR EACH STATEMENT</literal>. The following table
! summarises which types of triggers may be used on tables and views:
</para>
+ <informaltable id="supported-trigger-types">
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>When</entry>
+ <entry>Event</entry>
+ <entry>Row-level</entry>
+ <entry>Statement-level</entry>
+ </row>
+ </thead>
+ <tbody>
+ <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>
+ </row>
+ <row>
+ <entry align="center"><command>TRUNCATE</></entry>
+ <entry align="center">X</entry>
+ <entry align="center">Tables</entry>
+ </row>
+ <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>
+ </row>
+ <row>
+ <entry align="center"><command>TRUNCATE</></entry>
+ <entry align="center">X</entry>
+ <entry align="center">Tables</entry>
+ </row>
+ <row>
+ <entry align="center" morerows="1"><literal>INSTEAD OF</></entry>
+ <entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
+ <entry align="center">Views</entry>
+ <entry align="center">X</entry>
+ </row>
+ <row>
+ <entry align="center"><command>TRUNCATE</></entry>
+ <entry align="center">X</entry>
+ <entry align="center">X</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </informaltable>
+
<para>
! Also, most trigger definitions can specify a Boolean <literal>WHEN</>
condition, which will be tested to see whether the trigger should
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
is not so useful for them since the condition cannot refer to any values
! in the table. <literal>INSTEAD OF</> triggers cannot be used with
! <literal>WHEN</> conditions.
</para>
<para>
*************** CREATE TRIGGER <replaceable class="PARAM
*** 116,125 ****
<varlistentry>
<term><literal>BEFORE</literal></term>
<term><literal>AFTER</literal></term>
<listitem>
<para>
! Determines whether the function is called before or after the
! event.
</para>
</listitem>
</varlistentry>
--- 174,184 ----
<varlistentry>
<term><literal>BEFORE</literal></term>
<term><literal>AFTER</literal></term>
+ <term><literal>INSTEAD OF</literal></term>
<listitem>
<para>
! Determines whether the function is called before, after or instead of
! the event.
</para>
</listitem>
</varlistentry>
*************** UPDATE OF <replaceable>column_name1</rep
*** 143,148 ****
--- 202,213 ----
The trigger will only fire if at least one of the listed columns
is mentioned as a target of the update.
</para>
+
+ <para>
+ <literal>INSTEAD OF</> triggers do not support lists of columns. An
+ <literal>INSTEAD OF</> trigger must be unconditional, and is fired
+ for each affected row in the view.
+ </para>
</listitem>
</varlistentry>
*************** UPDATE OF <replaceable>column_name1</rep
*** 150,156 ****
<term><replaceable class="parameter">table</replaceable></term>
<listitem>
<para>
! The name (optionally schema-qualified) of the table the trigger
is for.
</para>
</listitem>
--- 215,221 ----
<term><replaceable class="parameter">table</replaceable></term>
<listitem>
<para>
! The name (optionally schema-qualified) of the table or view the trigger
is for.
</para>
</listitem>
*************** UPDATE OF <replaceable>column_name1</rep
*** 189,194 ****
--- 254,264 ----
</para>
<para>
+ <literal>INSTEAD OF</> triggers do not support <literal>WHEN</>
+ conditions.
+ </para>
+
+ <para>
Currently, <literal>WHEN</literal> expressions cannot contain
subqueries.
</para>
*************** CREATE TRIGGER log_update
*** 326,331 ****
--- 396,411 ----
WHEN (OLD.* IS DISTINCT FROM NEW.*)
EXECUTE PROCEDURE log_account_update();
</programlisting>
+
+ Execute the function <function>view_insert_row</> for each row to insert
+ rows into a view:
+
+ <programlisting>
+ CREATE TRIGGER view_insert
+ INSTEAD OF INSERT ON my_view
+ FOR EACH ROW
+ EXECUTE PROCEDURE view_insert_row();
+ </programlisting>
</para>
<para>
*************** CREATE TRIGGER log_update
*** 396,402 ****
<para>
The ability to fire triggers for <command>TRUNCATE</command> is a
! <productname>PostgreSQL</> extension of the SQL standard.
</para>
</refsect1>
--- 476,485 ----
<para>
The ability to fire triggers for <command>TRUNCATE</command> is a
! <productname>PostgreSQL</> extension of the SQL standard, as is the
! ability to define statement-level triggers on views that fire
! <literal>BEFORE</> or <literal>AFTER</> <command>INSERT</>,
! <command>UPDATE</> and <command>DELETE</> commands.
</para>
</refsect1>
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index c42ff29..ca91ac0 100644
*** a/doc/src/sgml/rules.sgml
--- b/doc/src/sgml/rules.sgml
***************
*** 168,176 ****
<para>
<command>DELETE</command> commands don't need a target list
because they don't produce any result. In fact, the planner will
! add a special <acronym>CTID</> entry to the empty target list, but
! this is after the rule system and will be discussed later; for the
! rule system, the target list is empty.
</para>
<para>
--- 168,178 ----
<para>
<command>DELETE</command> commands don't need a target list
because they don't produce any result. In fact, the planner will
! add a special <acronym>CTID</> entry to the empty target list (if the
! result relation is a table), but this is after the rule system and
! will be discussed later; for the rule system, the target list is empty.
! If the result relation is a view, the rule system will behave slightly
! differently, as described later.
</para>
<para>
***************
*** 189,198 ****
For <command>UPDATE</command> commands, the target list
describes the new rows that should replace the old ones. In the
rule system, it contains just the expressions from the <literal>SET
! column = expression</literal> part of the command. The planner will handle
! missing columns by inserting expressions that copy the values from
! the old row into the new one. And it will add the special
! <acronym>CTID</> entry just as for <command>DELETE</command>, too.
</para>
<para>
--- 191,202 ----
For <command>UPDATE</command> commands, the target list
describes the new rows that should replace the old ones. In the
rule system, it contains just the expressions from the <literal>SET
! column = expression</literal> part of the command. The planner will
! handle missing columns by inserting expressions that copy the values
! from the old row into the new one. And it will add the special
! <acronym>CTID</> entry, if the result relation is a table, just as
! for <command>DELETE</command>. If the result relation is a view,
! the rule system will behave slightly differently, as described later.
</para>
<para>
*************** SELECT shoe_ready.shoename, shoe_ready.s
*** 656,662 ****
<para>
Two details of the query tree aren't touched in the description of
view rules above. These are the command type and the result relation.
! In fact, view rules don't need this information.
</para>
<para>
--- 660,668 ----
<para>
Two details of the query tree aren't touched in the description of
view rules above. These are the command type and the result relation.
! In fact, the command type is not needed by view rules, but the result
! relation may affect the way in which the query rewriter works, because
! special care needs to be taken if the result relation is a view.
</para>
<para>
*************** SELECT t1.a, t2.b FROM t1, t2 WHERE t1.a
*** 725,745 ****
</programlisting>
will do. But there is a little problem in
! <command>UPDATE</command>: The executor does not care what the
! results from the join it is doing are meant for. It just produces
! a result set of rows. The difference that one is a
! <command>SELECT</command> command and the other is an
! <command>UPDATE</command> is handled in the caller of the
! executor. The caller still knows (looking at the query tree) that
! this is an <command>UPDATE</command>, and it knows that this
! result should go into table <literal>t1</>. But which of the rows that are
! there has to be replaced by the new row?
</para>
<para>
! To resolve this problem, another entry is added to the target list
! in <command>UPDATE</command> (and also in
! <command>DELETE</command>) statements: the current tuple ID
(<acronym>CTID</>).<indexterm><primary>CTID</></>
This is a system column containing the
file block number and position in the block for the row. Knowing
--- 731,750 ----
</programlisting>
will do. But there is a little problem in
! <command>UPDATE</command>: The branch of the executor plan that does
! the join does not care what the results from the join it is doing are
! meant for. It just produces a result set of rows. The difference that
! one is a <command>SELECT</command> command and the other is an
! <command>UPDATE</command> is handled higher up in the executor, where
! it knows that this is an <command>UPDATE</command>, and it knows that
! this result should go into table <literal>t1</>. But which of the rows
! that are there has to be replaced by the new row?
</para>
<para>
! If the result relation is a table, this problem is resolved by adding
! another entry to the target list in <command>UPDATE</command> (and
! also in <command>DELETE</command>) statements: the current tuple ID
(<acronym>CTID</>).<indexterm><primary>CTID</></>
This is a system column containing the
file block number and position in the block for the row. Knowing
*************** SELECT t1.a, t2.b, t1.ctid FROM t1, t2 W
*** 764,770 ****
<para>
Knowing all that, we can simply apply view rules in absolutely
! the same way to any command. There is no difference.
</para>
</sect2>
--- 769,777 ----
<para>
Knowing all that, we can simply apply view rules in absolutely
! the same way to any command. There is no difference, unless the view is
! the result relation, which must be handled differently (see
! <xref linkend="rules-views-update">).
</para>
</sect2>
*************** SELECT t1.a, t2.b, t1.ctid FROM t1, t2 W
*** 803,818 ****
<para>
What happens if a view is named as the target relation for an
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command>? After doing the substitutions
! described above, we will have a query tree in which the result
! relation points at a subquery range-table entry. This will not
! work, so the rewriter throws an error if it sees it has produced
! such a thing.
</para>
<para>
! To change this, we can define rules that modify the behavior of
! these kinds of commands. This is the topic of the next section.
</para>
</sect2>
--- 810,859 ----
<para>
What happens if a view is named as the target relation for an
<command>INSERT</command>, <command>UPDATE</command>, or
! <command>DELETE</command>? Simply doing the substitutions
! described above would give a query tree in which the result
! relation points at a subquery range-table entry, which would not
! work. The rewriter handles this case slightly differently, leaving
! the original view range-table entry as the result relation, and for
! <command>UPDATE</command> and <command>DELETE</command> operations,
! adding a new subquery range-table entry based on the view's query.
</para>
<para>
! Another problem that now arises is how to identify the rows to be
! updated in the result relation. Recall that when the result relation
! was a table, a special <acronym>CTID</> entry was added to the target
! list to identify the physical locations of the rows to be updated.
! This does not work if the result relation is a view, because a view
! typically does not have a <acronym>CTID</>, and its rows may not have
! unique physical locations. Instead, for an <command>UPDATE</command>
! or <command>DELETE</command> operation, a special <literal>wholerow</>
! entry is added to the target list, which expands to include all
! columns from the view. The executor does not know how to update the
! view; it relies on <literal>INSTEAD OF</> triggers for this. The
! executor uses the <literal>wholerow</> target list entry to pass the
! data from all the columns in the view to the triggers, to allow them
! to work out what to update.
! </para>
!
! <para>
! If there are no <literal>INSTEAD OF</> triggers to update the view,
! the executor will throw an error, because it cannot automatically
! update a view by itself. To change this, we can define rules that
! modify the behavior of <command>INSERT</command>,
! <command>UPDATE</command>, and <command>DELETE</command> commands on
! a view. These rules will rewrite the command, typically into a command
! that updates one or more tables, rather than views. This is the topic
! of the next section.
! </para>
!
! <para>
! Note that rules are evaluated first, rewriting the original query
! before it is planned and executed. Therefore, if a view has
! <literal>INSTEAD OF</> triggers as well as rules on <command>INSERT</>,
! <command>UPDATE</>, and <command>DELETE</>, then the rules will be
! evaluated first, and depending on the result, the triggers may not be
! used at all.
</para>
</sect2>
*************** GRANT SELECT ON phone_number TO secretar
*** 1868,1876 ****
</para>
<para>
! On the other hand, a trigger cannot be created on views because
! there is no real data in a view relation; however INSERT, UPDATE,
! and DELETE rules can be created on views.
</para>
<para>
--- 1909,1923 ----
</para>
<para>
! In this chapter, we focused on using rules to update views. All of
! the update rule examples in this chapter can also be implemented
! using <literal>INSTEAD OF</> triggers on the views (triggers defined
! to fire instead of the view update operations, with trigger functions
! that do the <command>INSERT</>, <command>UPDATE</> and
! <command>DELETE</> operations on the base tables). Writing such
! triggers is often easier than writing rules, particularly if complex
! logic is required to perform the update, however, it is possible that
! for bulk update operations, rules may perform better than triggers.
</para>
<para>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 3565bff..58296fd 100644
*** a/doc/src/sgml/trigger.sgml
--- b/doc/src/sgml/trigger.sgml
***************
*** 33,39 ****
<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 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.
--- 33,43 ----
<para>
A trigger is a specification that the database should automatically
execute a particular function whenever a certain type of operation is
! performed.
! </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,
or once per <acronym>SQL</acronym> statement.
***************
*** 46,51 ****
--- 50,68 ----
</para>
<para>
+ On views, triggers can be defined to execute instead of
+ <command>INSERT</command>, <command>UPDATE</command>, or
+ <command>DELETE</command> operations, and are fired once per row that
+ needs to be modified in the view. It is the responsibility of the
+ trigger's function to perform the necessary modifications to the
+ underlying base tables and, where appropriate, return the modified
+ row as it appears in the view. Triggers can also be defined to
+ execute once per <acronym>SQL</acronym> statement, before or after
+ <command>INSERT</command>, <command>UPDATE</command>, or
+ <command>DELETE</command> operations.
+ </para>
+
+ <para>
The trigger function must be defined before the trigger itself can be
created. The trigger function must be declared as a
function taking no arguments and returning type <literal>trigger</>.
***************
*** 74,91 ****
two types of triggers are sometimes called <firstterm>row-level</>
triggers and <firstterm>statement-level</> triggers,
respectively. Triggers on <command>TRUNCATE</command> may only be
! defined at statement-level.
</para>
<para>
! Triggers are also classified as <firstterm>before</> triggers and
! <firstterm>after</> triggers.
! Statement-level before triggers naturally fire before the
! statement starts to do anything, while statement-level after
! triggers fire at the very end of the statement. Row-level before
triggers fire immediately before a particular row is operated on,
! while row-level after triggers fire at the end of the statement
! (but before any statement-level after triggers).
</para>
<para>
--- 91,120 ----
two types of triggers are sometimes called <firstterm>row-level</>
triggers and <firstterm>statement-level</> triggers,
respectively. Triggers on <command>TRUNCATE</command> may only be
! defined at statement-level. On views, triggers that fire before or
! after may only be defined at statement-level, and triggers that fire
! instead of an <command>INSERT</command>, <command>UPDATE</command>,
! or <command>DELETE</command> may only be defined at row-level.
</para>
<para>
! Triggers are also classified according to whether they fire
! <firstterm>before</>, <firstterm>after</> or
! <firstterm>instead of</> the operation. These are referred to
! as <literal>BEFORE</> triggers, <literal>AFTER</> triggers and
! <literal>INSTEAD OF</> triggers respectively.
! Statement-level <literal>BEFORE</> triggers naturally fire before the
! 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
! immediately as each row in the view is identified as needing to be
! operated on (after any statement-level <literal>BEFORE</> triggers,
! and before any statement-level <literal>AFTER</> triggers on the view).
</para>
<para>
***************
*** 117,124 ****
</listitem>
</itemizedlist>
! A row-level before trigger that does not intend to cause either of
! these behaviors must be careful to return as its result the same
row that was passed in (that is, the <varname>NEW</varname> row
for <command>INSERT</command> and <command>UPDATE</command>
triggers, the <varname>OLD</varname> row for
--- 146,153 ----
</listitem>
</itemizedlist>
! A row-level <literal>BEFORE</> trigger that does not intend to cause
! either of these behaviors must be careful to return as its result the same
row that was passed in (that is, the <varname>NEW</varname> row
for <command>INSERT</command> and <command>UPDATE</command>
triggers, the <varname>OLD</varname> row for
***************
*** 126,131 ****
--- 155,176 ----
</para>
<para>
+ A row-level <literal>INSTEAD OF</> trigger should either return
+ <symbol>NULL</> to indicate that it did not modify any data from
+ the view's underlying base tables, or it should return the view
+ row that was passed in (the <varname>NEW</varname> row
+ for <command>INSERT</command> and <command>UPDATE</command>
+ operations, or the <varname>OLD</varname> row for
+ <command>DELETE</command> operations). A nonnull return value is
+ used to signal that the trigger performed the necessary data
+ modifications in the view, and will cause the count of the number
+ of rows affected in the command status to be incremented. For
+ <command>INSERT</> and <command>UPDATE</> operations, the trigger
+ may modify the <varname>NEW</> row before returning it, to support
+ <command>INSERT RETURNING</> and <command>UPDATE RETURNING</>.
+ </para>
+
+ <para>
The return value is ignored for row-level triggers fired after an
operation, and so they can return <symbol>NULL</>.
</para>
***************
*** 133,143 ****
<para>
If more than one trigger is defined for the same event on the same
relation, the triggers will be fired in alphabetical order by
! trigger name. In the case of before triggers, the
! possibly-modified row returned by each trigger becomes the input
! to the next trigger. If any before trigger returns
<symbol>NULL</>, the operation is abandoned for that row and subsequent
! triggers are not fired.
</para>
<para>
--- 178,189 ----
<para>
If more than one trigger is defined for the same event on the same
relation, the triggers will be fired in alphabetical order by
! trigger name. In the case of <literal>BEFORE</> and
! <literal>INSTEAD OF</> triggers, the possibly-modified row returned by
! each trigger becomes the input to the next trigger. If any
! <literal>BEFORE</> or <literal>INSTEAD OF</> trigger returns
<symbol>NULL</>, the operation is abandoned for that row and subsequent
! triggers are not fired (for that row).
</para>
<para>
***************
*** 146,176 ****
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. (Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
! is not so useful for them.) In a before trigger, the <literal>WHEN</>
condition is evaluated just before the function is or would be executed,
so using <literal>WHEN</> is not materially different from testing the
same condition at the beginning of the trigger function. However, in
! an after trigger, the <literal>WHEN</> condition is evaluated just after
! the row update occurs, and it determines whether an event is queued to
! fire the trigger at the end of statement. So when an after trigger's
<literal>WHEN</> condition does not return true, it is not necessary
to queue an event nor to re-fetch the row at end of statement. This
can result in significant speedups in statements that modify many
rows, if the trigger only needs to be fired for a few of the rows.
</para>
<para>
! Typically, row before triggers are used for checking or
modifying the data that will be inserted or updated. For example,
! a before trigger might be used to insert the current time into a
<type>timestamp</type> column, or to check that two elements of the row are
! consistent. Row after triggers are most sensibly
used to propagate the updates to other tables, or make consistency
checks against other tables. The reason for this division of labor is
! that an after trigger can be certain it is seeing the final value of the
! row, while a before trigger cannot; there might be other before triggers
! firing after it. If you have no specific reason to make a trigger before
! or after, the before case is more efficient, since the information about
the operation doesn't have to be saved until end of statement.
</para>
--- 192,228 ----
be fired. In row-level triggers the <literal>WHEN</> condition can
examine the old and/or new values of columns of the row. (Statement-level
triggers can also have <literal>WHEN</> conditions, although the feature
! is not so useful for them.) In a <literal>BEFORE</> trigger, the
! <literal>WHEN</>
condition is evaluated just before the function is or would be executed,
so using <literal>WHEN</> is not materially different from testing the
same condition at the beginning of the trigger function. However, in
! an <literal>AFTER</> trigger, the <literal>WHEN</> condition is evaluated
! just after the row update occurs, and it determines whether an event is
! queued to fire the trigger at the end of statement. So when an
! <literal>AFTER</> trigger's
<literal>WHEN</> condition does not return true, it is not necessary
to queue an event nor to re-fetch the row at end of statement. This
can result in significant speedups in statements that modify many
rows, if the trigger only needs to be fired for a few of the rows.
+ <literal>INSTEAD OF</> triggers on views do not support
+ <literal>WHEN</> conditions.
</para>
<para>
! Typically, row-level <literal>BEFORE</> triggers are used for checking or
modifying the data that will be inserted or updated. For example,
! a <literal>BEFORE</> trigger might be used to insert the current time into a
<type>timestamp</type> column, or to check that two elements of the row are
! consistent. Row-level <literal>AFTER</> triggers are most sensibly
used to propagate the updates to other tables, or make consistency
checks against other tables. The reason for this division of labor is
! that an <literal>AFTER</> trigger can be certain it is seeing the final
! value of the row, while a <literal>BEFORE</> trigger cannot; there might
! be other <literal>BEFORE</> triggers firing after it. If you have no
! specific reason to make a trigger <literal>BEFORE</> or
! <literal>AFTER</>, the <literal>BEFORE</> case is more efficient, since
! the information about
the operation doesn't have to be saved until end of statement.
</para>
***************
*** 237,243 ****
Statement-level triggers follow simple visibility rules: none of
the changes made by a statement are visible to statement-level
triggers that are invoked before the statement, whereas all
! modifications are visible to statement-level after triggers.
</para>
</listitem>
--- 289,296 ----
Statement-level triggers follow simple visibility rules: none of
the changes made by a statement are visible to statement-level
triggers that are invoked before the statement, whereas all
! modifications are visible to statement-level <literal>AFTER</>
! triggers.
</para>
</listitem>
***************
*** 245,258 ****
<para>
The data change (insertion, update, or deletion) causing the
trigger to fire is naturally <emphasis>not</emphasis> visible
! to SQL commands executed in a row-level before trigger, because
! it hasn't happened yet.
</para>
</listitem>
<listitem>
<para>
! However, SQL commands executed in a row-level before
trigger <emphasis>will</emphasis> see the effects of data
changes for rows previously processed in the same outer
command. This requires caution, since the ordering of these
--- 298,311 ----
<para>
The data change (insertion, update, or deletion) causing the
trigger to fire is naturally <emphasis>not</emphasis> visible
! to SQL commands executed in a row-level <literal>BEFORE</> trigger,
! because it hasn't happened yet.
</para>
</listitem>
<listitem>
<para>
! However, SQL commands executed in a row-level <literal>BEFORE</>
trigger <emphasis>will</emphasis> see the effects of data
changes for rows previously processed in the same outer
command. This requires caution, since the ordering of these
***************
*** 263,269 ****
<listitem>
<para>
! When a row-level after trigger is fired, all data changes made
by the outer command are already complete, and are visible to
the invoked trigger function.
</para>
--- 316,323 ----
<listitem>
<para>
! When a row-level <literal>AFTER</> trigger is fired, all data
! changes made
by the outer command are already complete, and are visible to
the invoked trigger function.
</para>
*************** typedef struct TriggerData
*** 378,383 ****
--- 432,446 ----
</varlistentry>
<varlistentry>
+ <term><literal>TRIGGER_FIRED_INSTEAD(tg_event)</literal></term>
+ <listitem>
+ <para>
+ Returns true if the trigger fired instead of the operation.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>TRIGGER_FIRED_AFTER(tg_event)</literal></term>
<listitem>
<para>
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 2b92e46..001f459 100644
*** a/src/backend/catalog/index.c
--- b/src/backend/catalog/index.c
*************** index_create(Oid heapRelationId,
*** 825,830 ****
--- 825,831 ----
trigger->funcname = SystemFuncName("unique_key_recheck");
trigger->args = NIL;
trigger->before = false;
+ trigger->instead = false;
trigger->row = true;
trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE;
trigger->columns = NIL;
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index bd81222..1641048 100644
*** a/src/backend/catalog/information_schema.sql
--- b/src/backend/catalog/information_schema.sql
*************** CREATE VIEW triggers AS
*** 1937,1943 ****
CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END
AS character_data) AS action_orientation,
CAST(
! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' ELSE 'AFTER' END
AS character_data) AS condition_timing,
CAST(null AS sql_identifier) AS condition_reference_old_table,
CAST(null AS sql_identifier) AS condition_reference_new_table,
--- 1937,1943 ----
CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END
AS character_data) AS action_orientation,
CAST(
! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' WHEN t.tgtype & 64 = 64 THEN 'INSTEAD OF' ELSE 'AFTER' END
AS character_data) AS condition_timing,
CAST(null AS sql_identifier) AS condition_reference_old_table,
CAST(null AS sql_identifier) AS condition_reference_new_table,
*************** CREATE VIEW views AS
*** 2233,2241 ****
THEN 'YES' ELSE 'NO' END
AS yes_or_no) AS is_insertable_into,
! CAST('NO' AS yes_or_no) AS is_trigger_updatable,
! CAST('NO' AS yes_or_no) AS is_trigger_deletable,
! CAST('NO' AS yes_or_no) AS is_trigger_insertable_into
FROM pg_namespace nc, pg_class c
--- 2233,2252 ----
THEN 'YES' ELSE 'NO' END
AS yes_or_no) AS is_insertable_into,
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 80 = 80)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_updatable,
!
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 72 = 72)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_deletable,
!
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 68 = 68)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_insertable_into
FROM pg_namespace nc, pg_class c
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index bfcfdfc..b733f4c 100644
*** a/src/backend/catalog/sql_features.txt
--- b/src/backend/catalog/sql_features.txt
*************** T211 Basic trigger capability 06 Support
*** 426,432 ****
T211 Basic trigger capability 07 TRIGGER privilege YES
T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted
T212 Enhanced trigger capability YES
! T213 INSTEAD OF triggers NO
T231 Sensitive cursors YES
T241 START TRANSACTION statement YES
T251 SET TRANSACTION statement: LOCAL option NO
--- 426,432 ----
T211 Basic trigger capability 07 TRIGGER privilege YES
T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted
T212 Enhanced trigger capability YES
! T213 INSTEAD OF triggers YES
T231 Sensitive cursors YES
T241 START TRANSACTION statement YES
T251 SET TRANSACTION statement: LOCAL option NO
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a5d7af6..9bcdbf0 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** CreateFKCheckTrigger(RangeVar *myRel, Co
*** 5691,5696 ****
--- 5691,5697 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = myRel;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
/* Either ON INSERT or ON UPDATE */
*************** createForeignKeyTriggers(Relation rel, C
*** 5754,5759 ****
--- 5755,5761 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_DELETE;
fk_trigger->columns = NIL;
*************** createForeignKeyTriggers(Relation rel, C
*** 5807,5812 ****
--- 5809,5815 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_UPDATE;
fk_trigger->columns = NIL;
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index f0b32dd..2c960e2 100644
*** a/src/backend/commands/trigger.c
--- b/src/backend/commands/trigger.c
*************** CreateTrigger(CreateTrigStmt *stmt, cons
*** 150,159 ****
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 150,198 ----
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind == RELKIND_RELATION)
! {
! /* Forbid INSTEAD OF triggers on tables */
! if (stmt->instead)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a table",
! RelationGetRelationName(rel)),
! errhint("Define INSTEAD OF triggers on views")));
! }
! else if (rel->rd_rel->relkind == RELKIND_VIEW)
! {
! /* Only allow INSTEAD OF or STATEMENT-level triggers on views */
! if (!stmt->instead && stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a view",
! RelationGetRelationName(rel)),
! errhint("Define ROW-level BEFORE and AFTER triggers on tables")));
!
! /* Disallow STATEMENT-level INSTEAD OF triggers */
! if (stmt->instead && !stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("STATEMENT-level INSTEAD OF triggers are not supported"),
! errhint("Use FOR EACH ROW for INSTEAD OF triggers")));
!
! /* Disallow WHEN clauses with INSTEAD OF triggers */
! if (stmt->instead && stmt->whenClause)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a WHEN condition with an INSTEAD OF trigger")));
!
! /* Disallow column selection with INSTEAD OF triggers */
! if (stmt->instead && stmt->columns != NIL)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a column list with an INSTEAD OF trigger")));
! }
! else
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
*************** CreateTrigger(CreateTrigStmt *stmt, cons
*** 188,193 ****
--- 227,234 ----
TRIGGER_CLEAR_TYPE(tgtype);
if (stmt->before)
TRIGGER_SETT_BEFORE(tgtype);
+ if (stmt->instead)
+ TRIGGER_SETT_INSTEAD(tgtype);
if (stmt->row)
TRIGGER_SETT_ROW(tgtype);
tgtype |= stmt->events;
*************** CreateTrigger(CreateTrigStmt *stmt, cons
*** 198,203 ****
--- 239,252 ----
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("TRUNCATE FOR EACH ROW triggers are not supported")));
+ /* Disallow TRUNCATE triggers on VIEWs */
+ if (rel->rd_rel->relkind == RELKIND_VIEW && TRIGGER_FOR_TRUNCATE(tgtype))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is a view",
+ RelationGetRelationName(rel)),
+ errhint("Define TRUNCATE triggers on tables")));
+
/*
* Parse the WHEN clause, if any
*/
*************** RemoveTriggerById(Oid trigOid)
*** 1031,1040 ****
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 1080,1090 ----
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION &&
! rel->rd_rel->relkind != RELKIND_VIEW)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
*************** InsertTrigger(TriggerDesc *trigdesc, Tri
*** 1506,1511 ****
--- 1556,1566 ----
n = trigdesc->n_before_row;
t = trigdesc->tg_before_row;
}
+ else if (TRIGGER_FOR_INSTEAD(trigger->tgtype))
+ {
+ n = trigdesc->n_instead_row;
+ t = trigdesc->tg_instead_row;
+ }
else
{
n = trigdesc->n_after_row;
*************** CopyTriggerDesc(TriggerDesc *trigdesc)
*** 1656,1661 ****
--- 1711,1729 ----
else
t[i] = NULL;
}
+ n = newdesc->n_instead_row;
+ t = newdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ {
+ if (n[i] > 0)
+ {
+ tnew = (int *) palloc(n[i] * sizeof(int));
+ memcpy(tnew, t[i], n[i] * sizeof(int));
+ t[i] = tnew;
+ }
+ else
+ t[i] = NULL;
+ }
n = newdesc->n_after_statement;
t = newdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
*************** FreeTriggerDesc(TriggerDesc *trigdesc)
*** 1698,1703 ****
--- 1766,1775 ----
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
pfree(t[i]);
+ t = trigdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ if (t[i] != NULL)
+ pfree(t[i]);
t = trigdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
*************** ExecARInsertTriggers(EState *estate, Res
*** 2000,2005 ****
--- 2072,2121 ----
true, NULL, trigtuple, recheckIndexes, NULL);
}
+ HeapTuple
+ ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_INSERT];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_INSERT];
+ TriggerData LocTriggerData;
+ HeapTuple newtuple = trigtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, NULL, trigtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != trigtuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
{
*************** ExecARDeleteTriggers(EState *estate, Res
*** 2134,2139 ****
--- 2250,2297 ----
}
}
+ bool
+ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_DELETE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_DELETE];
+ TriggerData LocTriggerData;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, trigtuple, NULL))
+ continue;
+
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple == NULL)
+ return false; /* Delete was suppressed */
+ if (rettuple != trigtuple)
+ heap_freetuple(rettuple);
+ }
+ return true;
+ }
+
void
ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
{
*************** ExecARUpdateTriggers(EState *estate, Res
*** 2281,2286 ****
--- 2439,2488 ----
}
}
+ HeapTuple
+ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple oldtuple, HeapTuple newtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_UPDATE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_UPDATE];
+ TriggerData LocTriggerData;
+ HeapTuple intuple = newtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, oldtuple, newtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = oldtuple;
+ LocTriggerData.tg_newtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != intuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
{
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 20af966..3a7b614 100644
*** a/src/backend/executor/execMain.c
--- b/src/backend/executor/execMain.c
*************** InitResultRelInfo(ResultRelInfo *resultR
*** 899,908 ****
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("cannot change view \"%s\"",
! RelationGetRelationName(resultRelationDesc))));
break;
default:
ereport(ERROR,
--- 899,905 ----
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! /* OK */
break;
default:
ereport(ERROR,
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index a9958eb..e6796b6 100644
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
*************** ExecProcessReturning(ProjectionInfo *pro
*** 149,154 ****
--- 149,200 ----
}
/* ----------------------------------------------------------------
+ * ExecCheckViewTriggers
+ *
+ * If the result relation is a view, check that it has the
+ * appropriate INSTEAD OF triggers to carry out the current
+ * operation.
+ * ----------------------------------------------------------------
+ */
+ static void
+ ExecCheckViewTriggers(ResultRelInfo *resultRelInfo,
+ CmdType operation)
+ {
+ TriggerDesc *trigDesc = resultRelInfo->ri_TrigDesc;
+
+ switch (operation)
+ {
+ case CMD_INSERT:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_INSERT] == 0)
+ ereport(ERROR,
+ (errmsg("cannot insert into a view"),
+ errhint("You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.")));
+ break;
+
+ case CMD_UPDATE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_UPDATE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot update a view"),
+ errhint("You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.")));
+ break;
+
+ case CMD_DELETE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_DELETE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot delete from a view"),
+ errhint("You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.")));
+ break;
+
+ default:
+ elog(ERROR, "unknown operation");
+ break;
+ }
+ }
+
+ /* ----------------------------------------------------------------
* ExecInsert
*
* For INSERT, we have to insert the tuple into the target relation
*************** ExecInsert(TupleTableSlot *slot,
*** 167,172 ****
--- 213,219 ----
Relation resultRelationDesc;
Oid newId;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* get the heap tuple out of the tuple table slot, making sure we have a
*************** ExecInsert(TupleTableSlot *slot,
*** 179,184 ****
--- 226,232 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
/*
* If the result relation has OIDs, force the tuple's OID to zero so that
*************** ExecInsert(TupleTableSlot *slot,
*** 195,202 ****
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /* BEFORE ROW INSERT Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
--- 243,251 ----
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /* BEFORE ROW INSERT Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
*************** ExecInsert(TupleTableSlot *slot,
*** 237,244 ****
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
(estate->es_processed)++;
estate->es_lastoid = newId;
--- 286,329 ----
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual insert
! */
! HeapTuple newtuple;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_INSERT);
! newtuple = ExecIRInsertTriggers(estate, resultRelInfo, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. 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(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! newId = InvalidOid;
! }
! else
! {
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
! }
(estate->es_processed)++;
estate->es_lastoid = newId;
*************** ExecInsert(TupleTableSlot *slot,
*** 251,258 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW INSERT Triggers */
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
--- 336,344 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW INSERT Triggers (tables only) */
! if (!resultRelIsView)
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
*************** ExecInsert(TupleTableSlot *slot,
*** 268,280 ****
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
--- 354,372 ----
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed.
! *
! * When deleting from a table, tupleid identifies the tuple to
! * 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.
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
*************** ExecDelete(ItemPointer tupleid,
*** 284,298 ****
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW DELETE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
--- 376,393 ----
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
+ bool resultRelIsView;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /* BEFORE ROW DELETE Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
*************** ExecDelete(ItemPointer tupleid,
*** 313,323 ****
* referential integrity updates in transaction-snapshot mode transactions.
*/
ldelete:;
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 408,443 ----
* referential integrity updates in transaction-snapshot mode transactions.
*/
ldelete:;
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual delete
! */
! HeapTupleData tuple;
! bool diddelete;
!
! tuple.t_data = oldtuple;
! tuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(tuple.t_self));
! tuple.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_DELETE);
! diddelete = ExecIRDeleteTriggers(estate, resultRelInfo, &tuple);
!
! if (!diddelete) /* trigger(s) did nothing */
! return NULL;
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
*************** ldelete:;
*** 367,374 ****
* anyway, since the tuple is still visible to other transactions.
*/
! /* AFTER ROW DELETE Triggers */
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 487,495 ----
* anyway, since the tuple is still visible to other transactions.
*/
! /* AFTER ROW DELETE Triggers (tables only) */
! if (!resultRelIsView)
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
*************** ldelete:;
*** 382,391 ****
HeapTupleData deltuple;
Buffer delbuffer;
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
--- 503,522 ----
HeapTupleData deltuple;
Buffer delbuffer;
! if (resultRelIsView)
! {
! deltuple.t_data = oldtuple;
! deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(deltuple.t_self));
! deltuple.t_tableOid = InvalidOid;
! }
! else
! {
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
! }
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
*************** ldelete:;
*** 395,401 ****
slot, planSlot);
ExecClearTuple(slot);
! ReleaseBuffer(delbuffer);
return rslot;
}
--- 526,533 ----
slot, planSlot);
ExecClearTuple(slot);
! if (!resultRelIsView)
! ReleaseBuffer(delbuffer);
return rslot;
}
*************** ldelete:;
*** 413,423 ****
--- 545,561 ----
* is, we don't want to get stuck in an infinite loop
* which corrupts your database..
*
+ * When updating a table, tupleid identifies the tuple to
+ * 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.
+ *
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecUpdate(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EPQState *epqstate,
*************** ExecUpdate(ItemPointer tupleid,
*** 430,435 ****
--- 568,574 ----
ItemPointerData update_ctid;
TransactionId update_xmax;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* abort the operation if not running transactions
*************** ExecUpdate(ItemPointer tupleid,
*** 448,456 ****
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW UPDATE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
--- 587,597 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /* BEFORE ROW UPDATE Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
*************** lreplace:;
*** 501,511 ****
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in transaction-snapshot mode transactions.
*/
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 642,696 ----
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in transaction-snapshot mode transactions.
*/
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual update
! */
! HeapTupleData oldtup;
! HeapTuple newtuple;
!
! oldtup.t_data = oldtuple;
! oldtup.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(oldtup.t_self));
! oldtup.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_UPDATE);
! newtuple = ExecIRUpdateTriggers(estate, resultRelInfo,
! &oldtup, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. 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(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
*************** lreplace:;
*** 568,576 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW UPDATE Triggers */
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
--- 753,762 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW UPDATE Triggers (tables only) */
! if (!resultRelIsView)
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
*************** ExecModifyTable(ModifyTableState *node)
*** 654,659 ****
--- 840,846 ----
TupleTableSlot *planSlot;
ItemPointer tupleid = NULL;
ItemPointerData tuple_ctid;
+ HeapTupleHeader oldtuple;
/*
* On first call, fire BEFORE STATEMENT triggers before proceeding.
*************** ExecModifyTable(ModifyTableState *node)
*** 713,735 ****
if (junkfilter != NULL)
{
/*
! * extract the 'ctid' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! /* shouldn't ever get a null result... */
! if (isNull)
! elog(ERROR, "ctid is NULL");
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
}
/*
--- 900,936 ----
if (junkfilter != NULL)
{
+ ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
+ Relation resultRel = resultRelInfo->ri_RelationDesc;
+
/*
! * extract the 'ctid' or 'wholerow' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "wholerow is NULL");
! oldtuple = DatumGetHeapTupleHeader(datum);
! }
! else
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "ctid is NULL");
!
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
! }
}
/*
*************** ExecModifyTable(ModifyTableState *node)
*** 745,755 ****
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, planSlot,
&node->mt_epqstate, estate);
break;
default:
--- 946,956 ----
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, oldtuple, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, oldtuple, planSlot,
&node->mt_epqstate, estate);
break;
default:
*************** ExecInitModifyTable(ModifyTable *node, E
*** 833,838 ****
--- 1034,1043 ----
i = 0;
foreach(l, node->plans)
{
+ Relation resultRel = estate->es_result_relation_info->ri_RelationDesc;
+ if (resultRel->rd_rel->relkind == RELKIND_VIEW)
+ ExecCheckViewTriggers(estate->es_result_relation_info, operation);
+
subplan = (Plan *) lfirst(l);
mtstate->mt_plans[i] = ExecInitNode(subplan, estate, eflags);
estate->es_result_relation_info++;
*************** ExecInitModifyTable(ModifyTable *node, E
*** 988,997 ****
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /* For UPDATE/DELETE, find the ctid junk attr now */
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
}
resultRelInfo->ri_junkFilter = j;
--- 1193,1216 ----
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /*
! * For UPDATE/DELETE of real tables, find the ctid junk
! * attr now. For VIEWs use the wholerow junk attr.
! */
! Relation resultRel = resultRelInfo->ri_RelationDesc;
!
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk wholerow column");
! }
! else
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
! }
}
resultRelInfo->ri_junkFilter = j;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 5bd0ef0..b59deec 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyCreateTrigStmt(CreateTrigStmt *from
*** 3244,3249 ****
--- 3244,3250 ----
COPY_NODE_FIELD(funcname);
COPY_NODE_FIELD(args);
COPY_SCALAR_FIELD(before);
+ COPY_SCALAR_FIELD(instead);
COPY_SCALAR_FIELD(row);
COPY_SCALAR_FIELD(events);
COPY_NODE_FIELD(columns);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index c7dd42d..1bc8df7 100644
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalCreateTrigStmt(CreateTrigStmt *a,
*** 1699,1704 ****
--- 1699,1705 ----
COMPARE_NODE_FIELD(funcname);
COMPARE_NODE_FIELD(args);
COMPARE_SCALAR_FIELD(before);
+ COMPARE_SCALAR_FIELD(instead);
COMPARE_SCALAR_FIELD(row);
COMPARE_SCALAR_FIELD(events);
COMPARE_NODE_FIELD(columns);
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 9e884cb..a1ce90b 100644
*** a/src/backend/optimizer/plan/planmain.c
--- b/src/backend/optimizer/plan/planmain.c
*************** query_planner(PlannerInfo *root, List *t
*** 179,184 ****
--- 179,193 ----
*/
add_base_rels_to_query(root, (Node *) parse->jointree);
+ /*
+ * If the query target is a VIEW, it won't be in the jointree, but we
+ * need a dummy RelOptInfo node for it. This need not have any stats in
+ * it because it always just goes at the top of the plan tree.
+ */
+ if (parse->resultRelation &&
+ root->simple_rel_array[parse->resultRelation] == NULL)
+ build_simple_rel(root, parse->resultRelation, RELOPT_OTHER_MEMBER_REL);
+
/*
* Examine the targetlist and join tree, adding entries to baserel
* targetlists for all referenced Vars, and generating PlaceHolderInfo
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index a8464a7..39aab11 100644
*** a/src/backend/optimizer/prep/preptlist.c
--- b/src/backend/optimizer/prep/preptlist.c
***************
*** 38,44 ****
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table);
/*
--- 38,44 ----
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel);
/*
*************** preprocess_targetlist(PlannerInfo *root,
*** 52,57 ****
--- 52,58 ----
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
+ bool result_is_view = false;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
*************** preprocess_targetlist(PlannerInfo *root,
*** 63,81 ****
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
}
/*
! * for heap_form_tuple to work, the targetlist must match the exact order
! * of the attributes. We also need to fill in any missing attributes. -ay
! * 10/94
*/
! if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
! tlist = expand_targetlist(tlist, command_type,
! result_relation, range_table);
/*
* for "update" and "delete" queries, add ctid of the result relation into
--- 64,100 ----
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
+ Relation rel;
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
+
+ /*
+ * Open the result relation. We assume that the rewriter already
+ * acquired at least AccessShareLock on it, so we need no lock here.
+ */
+ rel = heap_open(getrelid(result_relation, range_table), NoLock);
+ result_is_view = rel->rd_rel->relkind == RELKIND_VIEW;
+
+ /*
+ * for heap_form_tuple to work, the targetlist must match the exact
+ * order of the attributes. We also need to fill in any missing
+ * attributes. -ay 10/94
+ */
+ if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ tlist = expand_targetlist(tlist, command_type,
+ result_relation, rel);
+
+ heap_close(rel, NoLock);
}
/*
! * For an UPDATE, expand_targetlist already created a fresh tlist. For
! * DELETE, better do a listCopy so that we don't destructively modify
! * the original tlist (is this really necessary?).
*/
! if (command_type == CMD_DELETE)
! tlist = list_copy(tlist);
/*
* for "update" and "delete" queries, add ctid of the result relation into
*************** preprocess_targetlist(PlannerInfo *root,
*** 83,90 ****
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
*/
! if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
{
TargetEntry *tle;
Var *var;
--- 102,114 ----
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
+ *
+ * We don't do this if the result relation is a view, since that won't
+ * expose a ctid. The rewriter should have already added a wholerow TLE
+ * for the view's subselect.
*/
! if (!result_is_view &&
! (command_type == CMD_UPDATE || command_type == CMD_DELETE))
{
TargetEntry *tle;
Var *var;
*************** preprocess_targetlist(PlannerInfo *root,
*** 97,110 ****
pstrdup("ctid"),
true);
- /*
- * For an UPDATE, expand_targetlist already created a fresh tlist. For
- * DELETE, better do a listCopy so that we don't destructively modify
- * the original tlist (is this really necessary?).
- */
- if (command_type == CMD_DELETE)
- tlist = list_copy(tlist);
-
tlist = lappend(tlist, tle);
}
--- 121,126 ----
*************** preprocess_targetlist(PlannerInfo *root,
*** 169,187 ****
}
else
{
! /* Not a table, so we need the whole row as a junk var */
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
}
}
--- 185,227 ----
}
else
{
! bool exists = false;
! ListCell *l;
!
! /*
! * Not a table, so we need the whole row as a junk var. If the
! * query target is a view, the rewriter will have already added
! * a whole row var, so don't add another for that RTE.
! */
! foreach(l, tlist)
! {
! TargetEntry *tle = (TargetEntry *) lfirst(l);
!
! if (tle->resjunk &&
! IsA(tle->expr, Var) &&
! ((Var *) tle->expr)->varno == rc->rti &&
! strcmp(tle->resname, "wholerow") == 0)
! {
! exists = true;
! break;
! }
! }
!
! if (!exists)
! {
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
! }
}
}
*************** preprocess_targetlist(PlannerInfo *root,
*** 241,251 ****
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
- Relation rel;
int attrno,
numattrs;
--- 281,290 ----
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel)
{
List *new_tlist = NIL;
ListCell *tlist_item;
int attrno,
numattrs;
*************** expand_targetlist(List *tlist, int comma
*** 256,267 ****
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order. We assume
! * that the rewriter already acquired at least AccessShareLock on the
! * relation, so we need no lock here.
*/
- rel = heap_open(getrelid(result_relation, range_table), NoLock);
-
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
--- 295,302 ----
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order.
*/
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
*************** expand_targetlist(List *tlist, int comma
*** 399,406 ****
tlist_item = lnext(tlist_item);
}
- heap_close(rel, NoLock);
-
return new_tlist;
}
--- 434,439 ----
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index ad71d3a..16cbea2 100644
*** a/src/backend/optimizer/util/plancat.c
--- b/src/backend/optimizer/util/plancat.c
*************** estimate_rel_size(Relation rel, int32 *a
*** 441,447 ****
*tuples = 1;
break;
default:
! /* else it has no disk storage; probably shouldn't get here? */
*pages = 0;
*tuples = 0;
break;
--- 441,452 ----
*tuples = 1;
break;
default:
! /*
! * Else it has no disk storage; probably shouldn't get here,
! * unless this is a VIEW which is the query target, in which
! * case we don't care about the sizes, since it will always be
! * at the top of the plan tree.
! */
*pages = 0;
*tuples = 0;
break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4054cb1..9277f94 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** static RangeVar *makeRangeVarFromAnyName
*** 246,252 ****
%type <str> OptSchemaName
%type <list> OptSchemaEltList
! %type <boolean> TriggerActionTime TriggerForSpec opt_trusted opt_restart_seqs
%type <list> TriggerEvents TriggerOneEvent
%type <value> TriggerFuncArg
--- 246,253 ----
%type <str> OptSchemaName
%type <list> OptSchemaEltList
! %type <boolean> TriggerForSpec opt_trusted opt_restart_seqs
! %type <ival> TriggerActionTime
%type <list> TriggerEvents TriggerOneEvent
%type <value> TriggerFuncArg
*************** CreateTrigStmt:
*** 3448,3454 ****
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = $4;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
--- 3449,3456 ----
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = ($4 & 1) != 0;
! n->instead = ($4 & 2) != 0;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
*************** CreateTrigStmt:
*** 3470,3475 ****
--- 3472,3478 ----
n->funcname = $17;
n->args = $19;
n->before = FALSE;
+ n->instead = FALSE;
n->row = TRUE;
n->events = intVal(linitial($6));
n->columns = (List *) lsecond($6);
*************** CreateTrigStmt:
*** 3483,3490 ****
;
TriggerActionTime:
! BEFORE { $$ = TRUE; }
! | AFTER { $$ = FALSE; }
;
TriggerEvents:
--- 3486,3494 ----
;
TriggerActionTime:
! BEFORE { $$ = 1; }
! | AFTER { $$ = 0; }
! | INSTEAD OF { $$ = 2; }
;
TriggerEvents:
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index e917554..1c8d627 100644
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** adjustJoinTreeList(Query *parsetree, boo
*** 564,570 ****
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
--- 564,572 ----
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions. For an UPDATE on a VIEW, add tlist entries assigning any
! * unassigned attributes their current values. The RHS of such assignments
! * will be rewritten to refer to the view's subselect node.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
*************** rewriteTargetList(Query *parsetree, Rela
*** 724,729 ****
--- 726,753 ----
false);
}
+ /*
+ * For an UPDATE on a VIEW where the TLE is missing, assign the
+ * current value, which will be rewritten as a query from the view's
+ * subselect node by ApplyRetrieveRule().
+ */
+ if (new_tle == NULL && commandType == CMD_UPDATE &&
+ target_relation->rd_rel->relkind == RELKIND_VIEW)
+ {
+ Node *new_expr;
+
+ new_expr = (Node *) makeVar(parsetree->resultRelation,
+ attrno,
+ att_tup->atttypid,
+ att_tup->atttypmod,
+ 0);
+
+ new_tle = makeTargetEntry((Expr *) new_expr,
+ attrno,
+ pstrdup(NameStr(att_tup->attname)),
+ false);
+ }
+
if (new_tle)
new_tlist = lappend(new_tlist, new_tle);
}
*************** ApplyRetrieveRule(Query *parsetree,
*** 1149,1154 ****
--- 1173,1180 ----
RangeTblEntry *rte,
*subrte;
RowMarkClause *rc;
+ Var *var;
+ TargetEntry *tle;
if (list_length(rule->actions) != 1)
elog(ERROR, "expected just one rule action");
*************** ApplyRetrieveRule(Query *parsetree,
*** 1177,1220 ****
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! /*
! * VIEWs are really easy --- just plug the view query in as a subselect,
! * replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! /*
! * We move the view's permission check data down to its rangetable. The
! * checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as implicit
! * FOR UPDATE/SHARE, the same as the parser would have done if the view's
! * subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will be
! * made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
return parsetree;
}
--- 1203,1313 ----
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! if (rt_index == parsetree->resultRelation)
! {
! /*
! * We have a VIEW as the query target. For DELETE and UPDATE, we
! * add a new subselect RTE using the view's query and adjust any
! * VARs in the original query to point to this instead of the
! * original view RTE.
! *
! * We keep the original view in the rtable as the query target, and
! * any Vars in the returning list that reference it are left alone.
! *
! * The resulting jointree fromlist will not refer to the view RTE,
! * and so the planner won't try to join to it. This will result in
! * a plan with a ModifyTable node at the root, referring to the
! * original view relation, and a subselect based on the view's query
! * merged with any user conditions.
! *
! * For INSERTS we do nothing. The original view remains the query
! * target.
! */
! if (parsetree->commandType == CMD_DELETE ||
! parsetree->commandType == CMD_UPDATE)
! {
! List *returningList = parsetree->returningList;
! /*
! * Make a new subselect RTE from the view query and adjust the
! * the original query to point to this instead of the original
! * view, while preserving the view resultRelation and any
! * returningList Vars.
! */
! rte = copyObject(rt_fetch(rt_index, parsetree->rtable));
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! parsetree->rtable = lappend(parsetree->rtable, rte);
! parsetree->returningList = NIL;
! ChangeVarNodes((Node *) parsetree, rt_index,
! list_length(parsetree->rtable), 0);
!
! parsetree->resultRelation = rt_index;
! parsetree->returningList = returningList;
!
! /*
! * Add a "wholerow" junk TLE so that the executor can retrieve
! * the old VIEW tuples to pass to the INSTEAD OF triggers.
! */
! var = makeVar(list_length(parsetree->rtable),
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
!
! tle = makeTargetEntry((Expr *) var,
! list_length(parsetree->targetList) + 1,
! pstrdup("wholerow"),
! true);
! parsetree->targetList = lappend(parsetree->targetList, tle);
! }
! }
! else
! {
! /*
! * Selecting from the VIEW --- just plug the view query in as a
! * subselect, replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
!
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
!
! /*
! * We move the view's permission check data down to its rangetable.
! * The checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
!
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
!
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as
! * implicit FOR UPDATE/SHARE, the same as the parser would have done
! * if the view's subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will
! * be made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
! }
return parsetree;
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 578b9ce..6cbbada 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** pg_get_triggerdef_worker(Oid trigid, boo
*** 545,550 ****
--- 545,552 ----
if (TRIGGER_FOR_BEFORE(trigrec->tgtype))
appendStringInfo(&buf, "BEFORE");
+ else if (TRIGGER_FOR_INSTEAD(trigrec->tgtype))
+ appendStringInfo(&buf, "INSTEAD OF");
else
appendStringInfo(&buf, "AFTER");
if (TRIGGER_FOR_INSERT(trigrec->tgtype))
diff --git a/src/backend/utils/adt/tsvector_op.c b/src/backend/utils/adt/tsvector_op.c
index 399fee8..75687aa 100644
*** a/src/backend/utils/adt/tsvector_op.c
--- b/src/backend/utils/adt/tsvector_op.c
*************** tsvector_update_trigger(PG_FUNCTION_ARGS
*** 1259,1265 ****
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
--- 1259,1265 ----
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ff7d97a..08a16df 100644
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
*************** dumpTrigger(Archive *fout, TriggerInfo *
*** 12099,12104 ****
--- 12099,12106 ----
findx = 0;
if (TRIGGER_FOR_BEFORE(tginfo->tgtype))
appendPQExpBuffer(query, "BEFORE");
+ if (TRIGGER_FOR_INSTEAD(tginfo->tgtype))
+ appendPQExpBuffer(query, "INSTEAD OF");
else
appendPQExpBuffer(query, "AFTER");
if (TRIGGER_FOR_INSERT(tginfo->tgtype))
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 83e9845..70dc7be 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** describeOneTableDetails(const char *sche
*** 1820,1827 ****
--- 1820,1838 ----
}
PQclear(result);
}
+ }
+ /*
+ * Print triggers next.
+ * This is part of the footer information about a table, but may also
+ * apply to a view.
+ */
+ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v')
+ {
/* print triggers (but only user-defined triggers) */
+ PGresult *result = NULL;
+ int tuples = 0;
+
if (tableinfo.hastriggers)
{
printfPQExpBuffer(&buf,
*************** describeOneTableDetails(const char *sche
*** 1935,1942 ****
--- 1946,1963 ----
}
PQclear(result);
}
+ }
+ /*
+ * Done printing trigger information for view or table.
+ * Finish printing the footer information about a table.
+ */
+ if (tableinfo.relkind == 'r')
+ {
/* print inherited tables */
+ PGresult *result = NULL;
+ int tuples = 0;
+
printfPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i WHERE c.oid=i.inhparent AND i.inhrelid = '%s' ORDER BY inhseqno", oid);
result = PSQLexec(buf.data, false);
diff --git a/src/include/catalog/pg_trigger.h b/src/include/catalog/pg_trigger.h
index 46285ac..8eaeacf 100644
*** a/src/include/catalog/pg_trigger.h
--- b/src/include/catalog/pg_trigger.h
*************** typedef FormData_pg_trigger *Form_pg_tri
*** 91,96 ****
--- 91,97 ----
#define TRIGGER_TYPE_DELETE (1 << 3)
#define TRIGGER_TYPE_UPDATE (1 << 4)
#define TRIGGER_TYPE_TRUNCATE (1 << 5)
+ #define TRIGGER_TYPE_INSTEAD (1 << 6)
/* Macros for manipulating tgtype */
#define TRIGGER_CLEAR_TYPE(type) ((type) = 0)
*************** typedef FormData_pg_trigger *Form_pg_tri
*** 101,106 ****
--- 102,108 ----
#define TRIGGER_SETT_DELETE(type) ((type) |= TRIGGER_TYPE_DELETE)
#define TRIGGER_SETT_UPDATE(type) ((type) |= TRIGGER_TYPE_UPDATE)
#define TRIGGER_SETT_TRUNCATE(type) ((type) |= TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_SETT_INSTEAD(type) ((type) |= TRIGGER_TYPE_INSTEAD)
#define TRIGGER_FOR_ROW(type) ((type) & TRIGGER_TYPE_ROW)
#define TRIGGER_FOR_BEFORE(type) ((type) & TRIGGER_TYPE_BEFORE)
*************** typedef FormData_pg_trigger *Form_pg_tri
*** 108,112 ****
--- 110,115 ----
#define TRIGGER_FOR_DELETE(type) ((type) & TRIGGER_TYPE_DELETE)
#define TRIGGER_FOR_UPDATE(type) ((type) & TRIGGER_TYPE_UPDATE)
#define TRIGGER_FOR_TRUNCATE(type) ((type) & TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_FOR_INSTEAD(type) ((type) & TRIGGER_TYPE_INSTEAD)
#endif /* PG_TRIGGER_H */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 08bb22a..d94c8c7 100644
*** a/src/include/commands/trigger.h
--- b/src/include/commands/trigger.h
*************** typedef struct TriggerData
*** 53,58 ****
--- 53,59 ----
#define TRIGGER_EVENT_OPMASK 0x00000003
#define TRIGGER_EVENT_ROW 0x00000004
#define TRIGGER_EVENT_BEFORE 0x00000008
+ #define TRIGGER_EVENT_INSTEAD 0x00000040
/* More TriggerEvent flags, used only within trigger.c */
*************** typedef struct TriggerData
*** 84,91 ****
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event))
/*
* Definitions for the replication role based firing.
--- 85,95 ----
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
+ #define TRIGGER_FIRED_INSTEAD(event) \
+ ((TriggerEvent) (event) & TRIGGER_EVENT_INSTEAD)
+
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event) && !TRIGGER_FIRED_INSTEAD (event))
/*
* Definitions for the replication role based firing.
*************** extern void ExecARInsertTriggers(EState
*** 135,140 ****
--- 139,147 ----
ResultRelInfo *relinfo,
HeapTuple trigtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRInsertTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSDeleteTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASDeleteTriggers(EState *estate,
*************** extern bool ExecBRDeleteTriggers(EState
*** 146,151 ****
--- 153,161 ----
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid);
+ extern bool ExecIRDeleteTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSUpdateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASUpdateTriggers(EState *estate,
*************** extern void ExecARUpdateTriggers(EState
*** 160,165 ****
--- 170,179 ----
ItemPointer tupleid,
HeapTuple newtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRUpdateTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple oldtuple,
+ HeapTuple newtuple);
extern void ExecBSTruncateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASTruncateTriggers(EState *estate,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b2f0fef..4fdd001 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct CreateTrigStmt
*** 1609,1614 ****
--- 1609,1615 ----
List *funcname; /* qual. name of function to call */
List *args; /* list of (T_String) Values or NIL */
bool before; /* BEFORE/AFTER */
+ bool instead; /* INSTEAD OF (overrides BEFORE/AFTER) */
bool row; /* ROW/STATEMENT */
/* events uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */
int16 events; /* INSERT/UPDATE/DELETE/TRUNCATE */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 17ad888..ea8f78b 100644
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
*************** typedef struct TriggerDesc
*** 82,91 ****
--- 82,93 ----
uint16 n_before_statement[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_before_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ uint16 n_instead_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ int *tg_instead_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_statement[TRIGGER_NUM_EVENT_CLASSES];
/* The actual array of triggers is here */
diff --git a/src/pl/plperl/expected/plperl_trigger.out b/src/pl/plperl/expected/plperl_trigger.out
index b5af566..8cd74cb 100644
*** a/src/pl/plperl/expected/plperl_trigger.out
--- b/src/pl/plperl/expected/plperl_trigger.out
*************** NOTICE: $_TD->{when} = 'BEFORE'
*** 124,129 ****
--- 124,207 ----
CONTEXT: PL/Perl function "trigger_data"
DROP TRIGGER show_trigger_data_trig on trigger_test;
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'INSERT'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{new} = {'i' => '2', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ update trigger_test_view set v = 'update' where i = 1;
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'UPDATE'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{new} = {'i' => '1', 'v' => 'update'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ delete from trigger_test_view;
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'DELETE'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
DROP FUNCTION trigger_data();
CREATE OR REPLACE FUNCTION valid_id() RETURNS trigger AS $$
diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index cfad487..bb52bd6 100644
*** a/src/pl/plperl/plperl.c
--- b/src/pl/plperl/plperl.c
*************** plperl_trigger_build_args(FunctionCallIn
*** 954,959 ****
--- 954,961 ----
when = "BEFORE";
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
when = "AFTER";
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ when = "INSTEAD OF";
else
when = "UNKNOWN";
hv_store_string(hv, "when", newSVstring(when));
diff --git a/src/pl/plperl/sql/plperl_trigger.sql b/src/pl/plperl/sql/plperl_trigger.sql
index d55f79a..1e68759 100644
*** a/src/pl/plperl/sql/plperl_trigger.sql
--- b/src/pl/plperl/sql/plperl_trigger.sql
*************** update trigger_test set v = 'update' whe
*** 60,65 ****
--- 60,79 ----
delete from trigger_test;
DROP TRIGGER show_trigger_data_trig on trigger_test;
+
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i = 1;
+ delete from trigger_test_view;
+
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
DROP FUNCTION trigger_data();
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 6601320..1dc72a0 100644
*** a/src/pl/plpgsql/src/pl_exec.c
--- b/src/pl/plpgsql/src/pl_exec.c
*************** plpgsql_exec_trigger(PLpgSQL_function *f
*** 581,588 ****
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE or AFTER");
var->isnull = false;
var->freeval = true;
--- 581,590 ----
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ var->value = CStringGetTextDatum("INSTEAD OF");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE, AFTER or INSTEAD OF");
var->isnull = false;
var->freeval = true;
diff --git a/src/pl/plpython/expected/plpython_trigger.out b/src/pl/plpython/expected/plpython_trigger.out
index 3192ff1..275d0f7 100644
*** a/src/pl/plpython/expected/plpython_trigger.out
--- b/src/pl/plpython/expected/plpython_trigger.out
*************** NOTICE: TD[table_schema] => public
*** 294,304 ****
CONTEXT: PL/Python function "trigger_data"
NOTICE: TD[when] => BEFORE
CONTEXT: PL/Python function "trigger_data"
DROP FUNCTION trigger_data() CASCADE;
! NOTICE: drop cascades to 3 other objects
! DETAIL: drop cascades to trigger show_trigger_data_trig_before on table trigger_test
! drop cascades to trigger show_trigger_data_trig_after on table trigger_test
! drop cascades to trigger show_trigger_data_trig_stmt on table trigger_test
--
-- trigger error handling
--
--- 294,374 ----
CONTEXT: PL/Python function "trigger_data"
NOTICE: TD[when] => BEFORE
CONTEXT: PL/Python function "trigger_data"
+ DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
+ DROP TRIGGER show_trigger_data_trig_before on trigger_test;
+ DROP TRIGGER show_trigger_data_trig_after on trigger_test;
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => INSERT
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => {'i': 2, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => None
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
+ update trigger_test_view set v = 'update' where i = 1;
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => UPDATE
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => {'i': 1, 'v': 'update'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
+ delete from trigger_test_view;
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => DELETE
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => None
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
DROP FUNCTION trigger_data() CASCADE;
! NOTICE: drop cascades to trigger show_trigger_data_trig on view trigger_test_view
! DROP VIEW trigger_test_view;
! delete from trigger_test;
--
-- trigger error handling
--
diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c
index 6e35637..1d512e0 100644
*** a/src/pl/plpython/plpython.c
--- b/src/pl/plpython/plpython.c
*************** PLy_trigger_build_args(FunctionCallInfo
*** 846,851 ****
--- 846,853 ----
pltwhen = PyString_FromString("BEFORE");
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
pltwhen = PyString_FromString("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ pltwhen = PyString_FromString("INSTEAD OF");
else
{
elog(ERROR, "unrecognized WHEN tg_event: %u", tdata->tg_event);
diff --git a/src/pl/plpython/sql/plpython_trigger.sql b/src/pl/plpython/sql/plpython_trigger.sql
index c60a673..fba4db0 100644
*** a/src/pl/plpython/sql/plpython_trigger.sql
--- b/src/pl/plpython/sql/plpython_trigger.sql
*************** update trigger_test set v = 'update' whe
*** 99,106 ****
delete from trigger_test;
truncate table trigger_test;
! DROP FUNCTION trigger_data() CASCADE;
--
-- trigger error handling
--- 99,122 ----
delete from trigger_test;
truncate table trigger_test;
! DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
! DROP TRIGGER show_trigger_data_trig_before on trigger_test;
! DROP TRIGGER show_trigger_data_trig_after on trigger_test;
!
! insert into trigger_test values(1,'insert');
! CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
!
! CREATE TRIGGER show_trigger_data_trig
! INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
! FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i = 1;
+ delete from trigger_test_view;
+
+ DROP FUNCTION trigger_data() CASCADE;
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
--
-- trigger error handling
diff --git a/src/pl/tcl/expected/pltcl_queries.out b/src/pl/tcl/expected/pltcl_queries.out
index cdab74a..b585736 100644
*** a/src/pl/tcl/expected/pltcl_queries.out
--- b/src/pl/tcl/expected/pltcl_queries.out
*************** NOTICE: TG_table_name: trigger_test
*** 196,201 ****
--- 196,237 ----
NOTICE: TG_table_schema: public
NOTICE: TG_when: BEFORE
NOTICE: args: {23 skidoo}
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: NEW: {i: 2, v: insert}
+ NOTICE: OLD: {}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: INSERT
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ update trigger_test_view set v = 'update' where i=1;
+ NOTICE: NEW: {i: 1, v: update}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: UPDATE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ delete from trigger_test_view;
+ NOTICE: NEW: {}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: DELETE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
update trigger_test set v = 'update' where i = 1;
NOTICE: NEW: {i: 1, v: update}
NOTICE: OLD: {i: 1, v: insert}
diff --git a/src/pl/tcl/expected/pltcl_queries_1.out b/src/pl/tcl/expected/pltcl_queries_1.out
index 5788df7..e3fd24d 100644
*** a/src/pl/tcl/expected/pltcl_queries_1.out
--- b/src/pl/tcl/expected/pltcl_queries_1.out
*************** NOTICE: TG_table_name: trigger_test
*** 196,201 ****
--- 196,237 ----
NOTICE: TG_table_schema: public
NOTICE: TG_when: BEFORE
NOTICE: args: {23 skidoo}
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: NEW: {i: 2, v: insert}
+ NOTICE: OLD: {}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: INSERT
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ update trigger_test_view set v = 'update' where i=1;
+ NOTICE: NEW: {i: 1, v: update}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: UPDATE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ delete from trigger_test_view;
+ NOTICE: NEW: {}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: DELETE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
update trigger_test set v = 'update' where i = 1;
NOTICE: NEW: {i: 1, v: update}
NOTICE: OLD: {i: 1, v: insert}
diff --git a/src/pl/tcl/expected/pltcl_setup.out b/src/pl/tcl/expected/pltcl_setup.out
index e46c1c3..f577e66 100644
*** a/src/pl/tcl/expected/pltcl_setup.out
--- b/src/pl/tcl/expected/pltcl_setup.out
*************** create function check_pkey1_exists(int4,
*** 51,56 ****
--- 51,57 ----
-- dump trigger data
CREATE TABLE trigger_test
(i int, v text );
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
if { [info exists TG_relid] } {
*************** $_$;
*** 85,90 ****
--- 86,94 ----
CREATE TRIGGER show_trigger_data_trig
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ CREATE TRIGGER show_trigger_data_view_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
--
-- Trigger function on every change to T_pkey1
--
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 8c94c82..7bf79fa 100644
*** a/src/pl/tcl/pltcl.c
--- b/src/pl/tcl/pltcl.c
*************** pltcl_trigger_handler(PG_FUNCTION_ARGS)
*** 822,827 ****
--- 822,829 ----
Tcl_DStringAppendElement(&tcl_cmd, "BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
Tcl_DStringAppendElement(&tcl_cmd, "AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ Tcl_DStringAppendElement(&tcl_cmd, "INSTEAD OF");
else
elog(ERROR, "unrecognized WHEN tg_event: %u", trigdata->tg_event);
diff --git a/src/pl/tcl/sql/pltcl_queries.sql b/src/pl/tcl/sql/pltcl_queries.sql
index 3a99132..8dbb6ec 100644
*** a/src/pl/tcl/sql/pltcl_queries.sql
--- b/src/pl/tcl/sql/pltcl_queries.sql
*************** select * from T_pkey2 order by key1 usin
*** 76,80 ****
--- 76,85 ----
-- show dump of trigger data
insert into trigger_test values(1,'insert');
+
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i=1;
+ delete from trigger_test_view;
+
update trigger_test set v = 'update' where i = 1;
delete from trigger_test;
diff --git a/src/pl/tcl/sql/pltcl_setup.sql b/src/pl/tcl/sql/pltcl_setup.sql
index 4a581ed..a9370d2 100644
*** a/src/pl/tcl/sql/pltcl_setup.sql
--- b/src/pl/tcl/sql/pltcl_setup.sql
*************** create function check_pkey1_exists(int4,
*** 60,65 ****
--- 60,67 ----
CREATE TABLE trigger_test
(i int, v text );
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
if { [info exists TG_relid] } {
*************** CREATE TRIGGER show_trigger_data_trig
*** 96,101 ****
--- 98,106 ----
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ CREATE TRIGGER show_trigger_data_view_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
--
-- Trigger function on every change to T_pkey1
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index fe98079..36d632f 100644
*** a/src/test/regress/expected/triggers.out
--- b/src/test/regress/expected/triggers.out
*************** SELECT * FROM min_updates_test_oids;
*** 791,793 ****
--- 791,1419 ----
DROP TABLE min_updates_test;
DROP TABLE min_updates_test_oids;
+ --
+ -- Test triggers on views
+ --
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ ERROR: cannot insert into a view
+ HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+ UPDATE main_view SET b = 20 WHERE a = 50;
+ ERROR: cannot update a view
+ HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+ DELETE FROM main_view WHERE a = 50;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a = 51;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- VIEW trigger function
+ CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
+ LANGUAGE plpgsql AS $$
+ declare
+ argstr text := '';
+ begin
+ 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 '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr;
+
+ if TG_WHEN = 'INSTEAD OF' then
+ if TG_OP = 'INSERT' then
+ raise NOTICE 'NEW: %', NEW;
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ raise NOTICE 'OLD: %, NEW: %', OLD, NEW;
+ UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ raise NOTICE 'OLD: %', OLD;
+ DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end if;
+
+ RETURN NULL;
+ end;
+ $$;
+ -- Before row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ -- After row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ -- Truncate triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ -- INSTEAD OF triggers aren't allowed on tables
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ -- Don't support WHEN clauses with INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: cannot use a WHEN condition with an INSTEAD OF trigger
+ -- Don't support column-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: cannot use a column list with an INSTEAD OF trigger
+ -- Don't support statement-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: STATEMENT-level INSTEAD OF triggers are not supported
+ HINT: Use FOR EACH ROW for INSTEAD OF triggers
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+ -- Valid BEFORE statement VIEW triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
+ CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt');
+ CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt');
+ -- Valid AFTER statement VIEW triggers
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt');
+ CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt');
+ CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt');
+ \set QUIET false
+ -- Insert into view using trigger
+ INSERT INTO main_view VALUES (20, 30);
+ NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt)
+ NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins)
+ NOTICE: NEW: (20,30)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt)
+ INSERT 0 1
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+ NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt)
+ NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins)
+ NOTICE: NEW: (21,31)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt)
+ a | b
+ ----+----
+ 21 | 31
+ (1 row)
+
+ INSERT 0 1
+ -- Table trigger will prevent updates
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (20,30), NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 0
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (21,31), NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ a | b
+ ---+---
+ (0 rows)
+
+ UPDATE 0
+ -- Remove table trigger to allow updates
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ DROP TRIGGER
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (20,30), NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 1
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (21,31), NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ a | b
+ ----+----
+ 21 | 32
+ (1 row)
+
+ UPDATE 1
+ -- Before and after stmt triggers should fire even when no rows are affected
+ UPDATE main_view SET b = 0 WHERE false;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 0
+ -- Delete from view using trigger
+ DELETE FROM main_view WHERE a IN (20,21);
+ NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (21,10)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (20,31)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (21,32)
+ NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt)
+ DELETE 3
+ DELETE FROM main_view WHERE a = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (31,10)
+ NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt)
+ a | b
+ ----+----
+ 31 | 10
+ (1 row)
+
+ DELETE 1
+ \set QUIET true
+ -- Describe view should list triggers
+ \d main_view
+ View "public.main_view"
+ Column | Type | Modifiers
+ --------+---------+-----------
+ a | integer |
+ b | integer |
+ Triggers:
+ after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt')
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt')
+ before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt')
+ before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt')
+ instead_of_delete_trig INSTEAD OF DELETE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del')
+ instead_of_insert_trig INSTEAD OF INSERT ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd')
+
+ -- Test dropping view triggers
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d+ main_view
+ View "public.main_view"
+ Column | Type | Modifiers | Storage | Description
+ --------+---------+-----------+---------+-------------
+ a | integer | | plain |
+ b | integer | | plain |
+ View definition:
+ SELECT main_table.a, main_table.b
+ FROM main_table;
+ Triggers:
+ after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt')
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt')
+ before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt')
+ before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd')
+
+ DROP VIEW main_view;
+ --
+ -- Test triggers on a join view
+ --
+ CREATE TABLE country_table (
+ country_id serial primary key,
+ country_name text unique not null,
+ continent text not null
+ );
+ NOTICE: CREATE TABLE will create implicit sequence "country_table_country_id_seq" for serial column "country_table.country_id"
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "country_table_pkey" for table "country_table"
+ NOTICE: CREATE TABLE / UNIQUE will create implicit index "country_table_country_name_key" for table "country_table"
+ INSERT INTO country_table (country_name, continent)
+ VALUES ('Japan', 'Asia'),
+ ('UK', 'Europe'),
+ ('USA', 'North America')
+ RETURNING *;
+ country_id | country_name | continent
+ ------------+--------------+---------------
+ 1 | Japan | Asia
+ 2 | UK | Europe
+ 3 | USA | North America
+ (3 rows)
+
+ CREATE TABLE city_table (
+ city_id serial primary key,
+ city_name text not null,
+ population bigint,
+ country_id int references country_table
+ );
+ NOTICE: CREATE TABLE will create implicit sequence "city_table_city_id_seq" for serial column "city_table.city_id"
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "city_table_pkey" for table "city_table"
+ CREATE VIEW city_view AS
+ SELECT city_id, city_name, population, country_name, continent
+ FROM city_table ci
+ LEFT JOIN country_table co ON co.country_id = ci.country_id;
+ CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS NOT NULL then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+ else
+ NEW.continent := NULL;
+ end if;
+
+ if NEW.city_id IS NOT NULL then
+ INSERT INTO city_table
+ VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id);
+ else
+ INSERT INTO city_table(city_name, population, country_id)
+ VALUES(NEW.city_name, NEW.population, ctry_id)
+ RETURNING city_id INTO NEW.city_id;
+ end if;
+
+ RETURN NEW;
+ end;
+ $$;
+ CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_insert();
+ CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ DELETE FROM city_table WHERE city_id = OLD.city_id;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end;
+ $$;
+ CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_delete();
+ CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS DISTINCT FROM OLD.country_name then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population,
+ country_id = ctry_id
+ WHERE city_id = OLD.city_id;
+ else
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population
+ WHERE city_id = OLD.city_id;
+ NEW.continent := OLD.continent;
+ end if;
+
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end;
+ $$;
+ CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_update();
+ -- INSERT .. RETURNING
+ \set QUIET false
+ INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 2 | London | 7556900 | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+---------------+------------+--------------+---------------
+ 3 | Washington DC | | USA | North America
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 123456 | New York | | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ INSERT 0 1
+ -- UPDATE .. RETURNING
+ UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error
+ ERROR: No such country: "Japon"
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match
+ UPDATE 0
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | | Japan | Asia
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | 13010279 | Japan | Asia
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 123456 | New York | | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+---------------
+ 123456 | New York | 8391881 | USA | North America
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2
+ WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *;
+ city_id | city_name | population | country_name | continent | city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------+---------+------------+------------+--------------+-----------
+ 2 | London | 7556900 | UK | Europe | 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ -- DELETE .. RETURNING
+ DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ DELETE 1
+ \set QUIET true
+ -- read-only view with WHERE clause
+ CREATE VIEW european_city_view AS
+ SELECT * FROM city_view WHERE continent = 'Europe';
+ SELECT count(*) FROM european_city_view;
+ count
+ -------
+ 1
+ (1 row)
+
+ CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql
+ AS 'begin RETURN NULL; end';
+ CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE
+ ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn();
+ \set QUIET false
+ INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z');
+ INSERT 0 0
+ UPDATE european_city_view SET population = 10000;
+ UPDATE 0
+ DELETE FROM european_city_view;
+ DELETE 0
+ \set QUIET true
+ -- rules bypassing no-op triggers
+ CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view
+ DO INSTEAD INSERT INTO city_view
+ VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent)
+ RETURNING *;
+ CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view
+ DO INSTEAD UPDATE city_view SET
+ city_name = NEW.city_name,
+ population = NEW.population,
+ country_name = NEW.country_name
+ WHERE city_id = OLD.city_id
+ RETURNING NEW.*;
+ CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view
+ DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *;
+ -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are
+ \set QUIET false
+ INSERT INTO european_city_view(city_name, country_name)
+ VALUES ('Cambridge', 'USA') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+---------------
+ 4 | Cambridge | | USA | North America
+ (1 row)
+
+ INSERT 0 1
+ UPDATE european_city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge';
+ UPDATE 0
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge';
+ DELETE 0
+ -- UPDATE and DELETE via rule and trigger
+ UPDATE city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE european_city_view SET population = 122800
+ WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | 122800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | 122800 | UK | Europe
+ (1 row)
+
+ DELETE 1
+ -- join UPDATE test
+ UPDATE city_view v SET population = 599657
+ FROM city_table ci, country_table co
+ WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA'
+ AND v.city_id = ci.city_id AND v.country_name = co.country_name
+ RETURNING co.country_id, v.country_name,
+ v.city_id, v.city_name, v.population;
+ country_id | country_name | city_id | city_name | population
+ ------------+--------------+---------+---------------+------------
+ 3 | USA | 3 | Washington DC | 599657
+ (1 row)
+
+ UPDATE 1
+ \set QUIET true
+ SELECT * FROM city_view;
+ city_id | city_name | population | country_name | continent
+ ---------+---------------+------------+--------------+---------------
+ 1 | Tokyo | 13010279 | Japan | Asia
+ 123456 | New York | 8391881 | USA | North America
+ 2 | London | 7556900 | UK | Europe
+ 3 | Washington DC | 599657 | USA | North America
+ (4 rows)
+
+ DROP TABLE city_table CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to view city_view
+ drop cascades to view european_city_view
+ DROP TABLE country_table;
diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c
index 8b0f9f1..4b4ede0 100644
*** a/src/test/regress/regress.c
--- b/src/test/regress/regress.c
*************** ttdummy(PG_FUNCTION_ARGS)
*** 489,495 ****
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
--- 489,495 ----
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 68495ca..33fb7f4 100644
*** a/src/test/regress/sql/triggers.sql
--- b/src/test/regress/sql/triggers.sql
*************** SELECT * FROM min_updates_test_oids;
*** 578,580 ****
--- 578,937 ----
DROP TABLE min_updates_test;
DROP TABLE min_updates_test_oids;
+
+ --
+ -- Test triggers on views
+ --
+
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ UPDATE main_view SET b = 20 WHERE a = 50;
+ DELETE FROM main_view WHERE a = 50;
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a = 51;
+
+ -- VIEW trigger function
+ CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
+ LANGUAGE plpgsql AS $$
+ declare
+ argstr text := '';
+ begin
+ 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 '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr;
+
+ if TG_WHEN = 'INSTEAD OF' then
+ if TG_OP = 'INSERT' then
+ raise NOTICE 'NEW: %', NEW;
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ raise NOTICE 'OLD: %, NEW: %', OLD, NEW;
+ UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ raise NOTICE 'OLD: %', OLD;
+ DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end if;
+
+ RETURN NULL;
+ end;
+ $$;
+
+ -- Before row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ -- After row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ -- Truncate triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ -- INSTEAD OF triggers aren't allowed on tables
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+
+ -- Don't support WHEN clauses with INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Don't support column-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Don't support statement-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+
+ -- Valid BEFORE statement VIEW triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
+
+ CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt');
+
+ CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt');
+
+ -- Valid AFTER statement VIEW triggers
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt');
+
+ CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt');
+
+ CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt');
+
+ \set QUIET false
+
+ -- Insert into view using trigger
+ INSERT INTO main_view VALUES (20, 30);
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+
+ -- Table trigger will prevent updates
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+
+ -- Remove table trigger to allow updates
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+
+ -- Before and after stmt triggers should fire even when no rows are affected
+ UPDATE main_view SET b = 0 WHERE false;
+
+ -- Delete from view using trigger
+ DELETE FROM main_view WHERE a IN (20,21);
+ DELETE FROM main_view WHERE a = 31 RETURNING a, b;
+
+ \set QUIET true
+
+ -- Describe view should list triggers
+ \d main_view
+
+ -- Test dropping view triggers
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d+ main_view
+ DROP VIEW main_view;
+
+ --
+ -- Test triggers on a join view
+ --
+ CREATE TABLE country_table (
+ country_id serial primary key,
+ country_name text unique not null,
+ continent text not null
+ );
+
+ INSERT INTO country_table (country_name, continent)
+ VALUES ('Japan', 'Asia'),
+ ('UK', 'Europe'),
+ ('USA', 'North America')
+ RETURNING *;
+
+ CREATE TABLE city_table (
+ city_id serial primary key,
+ city_name text not null,
+ population bigint,
+ country_id int references country_table
+ );
+
+ CREATE VIEW city_view AS
+ SELECT city_id, city_name, population, country_name, continent
+ FROM city_table ci
+ LEFT JOIN country_table co ON co.country_id = ci.country_id;
+
+ CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS NOT NULL then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+ else
+ NEW.continent := NULL;
+ end if;
+
+ if NEW.city_id IS NOT NULL then
+ INSERT INTO city_table
+ VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id);
+ else
+ INSERT INTO city_table(city_name, population, country_id)
+ VALUES(NEW.city_name, NEW.population, ctry_id)
+ RETURNING city_id INTO NEW.city_id;
+ end if;
+
+ RETURN NEW;
+ end;
+ $$;
+
+ CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_insert();
+
+ CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ DELETE FROM city_table WHERE city_id = OLD.city_id;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end;
+ $$;
+
+ CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_delete();
+
+ CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS DISTINCT FROM OLD.country_name then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population,
+ country_id = ctry_id
+ WHERE city_id = OLD.city_id;
+ else
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population
+ WHERE city_id = OLD.city_id;
+ NEW.continent := OLD.continent;
+ end if;
+
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end;
+ $$;
+
+ CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_update();
+
+ -- INSERT .. RETURNING
+ \set QUIET false
+
+ INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *;
+ INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *;
+ INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *;
+ INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *;
+ INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *;
+
+ -- UPDATE .. RETURNING
+ UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK
+
+ UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *;
+ UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *;
+ UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *;
+ UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *;
+ UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2
+ WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *;
+
+ -- DELETE .. RETURNING
+ DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *;
+
+ \set QUIET true
+
+ -- read-only view with WHERE clause
+ CREATE VIEW european_city_view AS
+ SELECT * FROM city_view WHERE continent = 'Europe';
+ SELECT count(*) FROM european_city_view;
+
+ CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql
+ AS 'begin RETURN NULL; end';
+
+ CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE
+ ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn();
+
+ \set QUIET false
+
+ INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z');
+ UPDATE european_city_view SET population = 10000;
+ DELETE FROM european_city_view;
+
+ \set QUIET true
+
+ -- rules bypassing no-op triggers
+ CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view
+ DO INSTEAD INSERT INTO city_view
+ VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent)
+ RETURNING *;
+
+ CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view
+ DO INSTEAD UPDATE city_view SET
+ city_name = NEW.city_name,
+ population = NEW.population,
+ country_name = NEW.country_name
+ WHERE city_id = OLD.city_id
+ RETURNING NEW.*;
+
+ CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view
+ DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *;
+
+ -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are
+ \set QUIET false
+
+ INSERT INTO european_city_view(city_name, country_name)
+ VALUES ('Cambridge', 'USA') RETURNING *;
+ UPDATE european_city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge';
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge';
+
+ -- UPDATE and DELETE via rule and trigger
+ UPDATE city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge' RETURNING *;
+ UPDATE european_city_view SET population = 122800
+ WHERE city_name = 'Cambridge' RETURNING *;
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *;
+
+ -- join UPDATE test
+ UPDATE city_view v SET population = 599657
+ FROM city_table ci, country_table co
+ WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA'
+ AND v.city_id = ci.city_id AND v.country_name = co.country_name
+ RETURNING co.country_id, v.country_name,
+ v.city_id, v.city_name, v.population;
+
+ \set QUIET true
+
+ SELECT * FROM city_view;
+
+ DROP TABLE city_table CASCADE;
+ DROP TABLE country_table;
On 29 September 2010 20:18, Bernd Helmle <mailings@oopsware.de> wrote:
--On 8. September 2010 09:00:33 +0100 Dean Rasheed
<dean.a.rasheed@gmail.com> wrote:Here's an updated version with improved formatting and a few minor
wording changes to the triggers chapter.This version doesn't apply clean anymore due to some rejects in plainmain.c.
Corrected version attached.
Ah yes, those pesky bits have been rotting while I wasn't looking.
Thanks for fixing them!
Regards,
Dean
Bernd Helmle <mailings@oopsware.de> writes:
--On 8. September 2010 09:00:33 +0100 Dean Rasheed
<dean.a.rasheed@gmail.com> wrote:Here's an updated version with improved formatting and a few minor
wording changes to the triggers chapter.
This version doesn't apply clean anymore due to some rejects in
plainmain.c. Corrected version attached.
Applied with revisions. A couple of points worth remarking:
* I took out this change in planmain.c:
+ /*
+ * If the query target is a VIEW, it won't be in the jointree, but we
+ * need a dummy RelOptInfo node for it. This need not have any stats in
+ * it because it always just goes at the top of the plan tree.
+ */
+ if (parse->resultRelation &&
+ root->simple_rel_array[parse->resultRelation] == NULL)
+ build_simple_rel(root, parse->resultRelation, RELOPT_OTHER_MEMBER_REL);
AFAICT that's just dead code: the only reason to build such a rel would
be if there were Vars referencing it in the main part of the plan tree.
But there aren't. Perhaps this was left over from some early iteration
of the patch before you had the Var numbering done right? Do you know
of any cases where it's still needed?
* I also took out the changes in preprocess_targetlist() that tried to
prevent equivalent wholerow vars from getting entered in the targetlist.
This would not work as intended since the executor has specific
expectations for the names of those junk TLEs; it'd fail if it ever
actually tried to do an EvalPlanQual recheck that needed those TLEs.
Now I believe that an EPQ recheck is impossible so far as the update or
delete itself is concerned, when the target is a view. So if you were
really concerned about the extra vars, the non-kluge route to a solution
would be to avoid generating RowMarks in the first place. You'd have to
think a bit about the possibility of SELECT FOR UPDATE in sub-selects
though; the query as a whole might need some rowmarks even if the top
level Modify node doesn't. On the whole I couldn't get excited about
this issue, so I just left it alone.
regards, tom lane
On 10 October 2010 19:06, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Applied with revisions.
Brilliant! Thank you very much.
* I took out this change in planmain.c:
+ /* + * If the query target is a VIEW, it won't be in the jointree, but we + * need a dummy RelOptInfo node for it. This need not have any stats in + * it because it always just goes at the top of the plan tree. + */ + if (parse->resultRelation && + root->simple_rel_array[parse->resultRelation] == NULL) + build_simple_rel(root, parse->resultRelation, RELOPT_OTHER_MEMBER_REL);AFAICT that's just dead code: the only reason to build such a rel would
be if there were Vars referencing it in the main part of the plan tree.
But there aren't. Perhaps this was left over from some early iteration
of the patch before you had the Var numbering done right? Do you know
of any cases where it's still needed?
No, I think you're right. It was just the leftovers of an early
attempt to get the rewriter changes right.
* I also took out the changes in preprocess_targetlist() that tried to
prevent equivalent wholerow vars from getting entered in the targetlist.
This would not work as intended since the executor has specific
expectations for the names of those junk TLEs; it'd fail if it ever
actually tried to do an EvalPlanQual recheck that needed those TLEs.
Ah yes, I missed that. I still don't see where it uses those TLEs by
name though. It looks as though it's using wholeAttNo, so perhaps my
code would have worked if I had set wholeAttNo on the RowMark? Anyway,
I don't think it's likely that this extra Var is going to be present
often in practice, so I don't think it's a problem worth worrying
about.
Thanks very much for looking at this.
Regards,
Dean
Show quoted text
Now I believe that an EPQ recheck is impossible so far as the update or
delete itself is concerned, when the target is a view. So if you were
really concerned about the extra vars, the non-kluge route to a solution
would be to avoid generating RowMarks in the first place. You'd have to
think a bit about the possibility of SELECT FOR UPDATE in sub-selects
though; the query as a whole might need some rowmarks even if the top
level Modify node doesn't. On the whole I couldn't get excited about
this issue, so I just left it alone.regards, tom lane