*** ./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 OptSchemaName %type OptSchemaEltList ! %type TriggerActionTime TriggerForSpec opt_trusted opt_restart_seqs %type TriggerEvents TriggerOneEvent %type TriggerFuncArg --- 245,252 ---- %type OptSchemaName %type OptSchemaEltList ! %type TriggerForSpec opt_trusted opt_restart_seqs ! %type TriggerActionTime %type TriggerEvents TriggerOneEvent %type 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;