diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2010-10-10 13:43:33 -0400 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2010-10-10 13:45:07 -0400 |
commit | 2ec993a7cbdd8e251817ac6bbc9a704ce8346f73 (patch) | |
tree | 1568fb4b00b6fa7997755113a3d0bbfead45c1fb /src/backend/executor/nodeModifyTable.c | |
parent | f7b15b5098ee89a2628129fbbef9901bded9d27b (diff) | |
download | postgresql-2ec993a7cbdd8e251817ac6bbc9a704ce8346f73.tar.gz postgresql-2ec993a7cbdd8e251817ac6bbc9a704ce8346f73.zip |
Support triggers on views.
This patch adds the SQL-standard concept of an INSTEAD OF trigger, which
is fired instead of performing a physical insert/update/delete. The
trigger function is passed the entire old and/or new rows of the view,
and must figure out what to do to the underlying tables to implement
the update. So this feature can be used to implement updatable views
using trigger programming style rather than rule hacking.
In passing, this patch corrects the names of some columns in the
information_schema.triggers view. It seems the SQL committee renamed
them somewhere between SQL:99 and SQL:2003.
Dean Rasheed, reviewed by Bernd Helmle; some additional hacking by me.
Diffstat (limited to 'src/backend/executor/nodeModifyTable.c')
-rw-r--r-- | src/backend/executor/nodeModifyTable.c | 499 |
1 files changed, 323 insertions, 176 deletions
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index a9958ebf394..541adaf6806 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -197,7 +197,7 @@ ExecInsert(TupleTableSlot *slot, /* BEFORE ROW INSERT Triggers */ if (resultRelInfo->ri_TrigDesc && - resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0) + resultRelInfo->ri_TrigDesc->trig_insert_before_row) { HeapTuple newtuple; @@ -225,32 +225,66 @@ ExecInsert(TupleTableSlot *slot, } } - /* - * Check the constraints of the tuple - */ - if (resultRelationDesc->rd_att->constr) - ExecConstraints(resultRelInfo, slot, estate); + /* INSTEAD OF ROW INSERT Triggers */ + if (resultRelInfo->ri_TrigDesc && + resultRelInfo->ri_TrigDesc->trig_insert_instead_row) + { + HeapTuple newtuple; - /* - * insert the tuple - * - * 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); + newtuple = ExecIRInsertTriggers(estate, resultRelInfo, tuple); + + if (newtuple == NULL) /* "do 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 + { + /* + * Check the constraints of the tuple + */ + if (resultRelationDesc->rd_att->constr) + ExecConstraints(resultRelInfo, slot, estate); + + /* + * insert the tuple + * + * 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); + + /* + * insert index entries for tuple + */ + if (resultRelInfo->ri_NumIndices > 0) + recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), + estate); + } (estate->es_processed)++; estate->es_lastoid = newId; setLastTid(&(tuple->t_self)); - /* - * insert index entries for tuple - */ - if (resultRelInfo->ri_NumIndices > 0) - recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), - estate); - /* AFTER ROW INSERT Triggers */ ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes); @@ -268,13 +302,19 @@ ExecInsert(TupleTableSlot *slot, * ExecDelete * * DELETE is like UPDATE, except that we delete the tuple and no - * index modifications are needed + * 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) @@ -293,7 +333,7 @@ ExecDelete(ItemPointer tupleid, /* BEFORE ROW DELETE Triggers */ if (resultRelInfo->ri_TrigDesc && - resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0) + resultRelInfo->ri_TrigDesc->trig_delete_before_row) { bool dodelete; @@ -304,69 +344,91 @@ ExecDelete(ItemPointer tupleid, return NULL; } - /* - * delete the tuple - * - * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check that - * the row to be deleted is visible to that snapshot, and throw a can't- - * serialize error if not. This is a special-case behavior needed for - * 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) + /* INSTEAD OF ROW DELETE Triggers */ + if (resultRelInfo->ri_TrigDesc && + resultRelInfo->ri_TrigDesc->trig_delete_instead_row) { - case HeapTupleSelfUpdated: - /* already deleted by self; nothing to do */ + HeapTupleData tuple; + bool dodelete; + + Assert(oldtuple != NULL); + tuple.t_data = oldtuple; + tuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple); + ItemPointerSetInvalid(&(tuple.t_self)); + tuple.t_tableOid = InvalidOid; + + dodelete = ExecIRDeleteTriggers(estate, resultRelInfo, &tuple); + + if (!dodelete) /* "do nothing" */ return NULL; + } + else + { + /* + * delete the tuple + * + * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check + * that the row to be deleted is visible to that snapshot, and throw a + * can't-serialize error if not. This is a special-case behavior + * needed for 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: + /* already deleted by self; nothing to do */ + return NULL; - case HeapTupleMayBeUpdated: - break; + case HeapTupleMayBeUpdated: + break; - case HeapTupleUpdated: - if (IsolationUsesXactSnapshot()) - ereport(ERROR, - (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("could not serialize access due to concurrent update"))); - if (!ItemPointerEquals(tupleid, &update_ctid)) - { - TupleTableSlot *epqslot; - - epqslot = EvalPlanQual(estate, - epqstate, - resultRelationDesc, - resultRelInfo->ri_RangeTableIndex, - &update_ctid, - update_xmax); - if (!TupIsNull(epqslot)) + case HeapTupleUpdated: + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent update"))); + if (!ItemPointerEquals(tupleid, &update_ctid)) { - *tupleid = update_ctid; - goto ldelete; + TupleTableSlot *epqslot; + + epqslot = EvalPlanQual(estate, + epqstate, + resultRelationDesc, + resultRelInfo->ri_RangeTableIndex, + &update_ctid, + update_xmax); + if (!TupIsNull(epqslot)) + { + *tupleid = update_ctid; + goto ldelete; + } } - } - /* tuple already deleted; nothing to do */ - return NULL; + /* tuple already deleted; nothing to do */ + return NULL; - default: - elog(ERROR, "unrecognized heap_delete status: %u", result); - return NULL; + default: + elog(ERROR, "unrecognized heap_delete status: %u", result); + return NULL; + } + + /* + * Note: Normally one would think that we have to delete index tuples + * associated with the heap tuple now... + * + * ... but in POSTGRES, we have no need to do this because VACUUM will + * take care of it later. We can't delete index tuples immediately + * anyway, since the tuple is still visible to other transactions. + */ } (estate->es_processed)++; - /* - * Note: Normally one would think that we have to delete index tuples - * associated with the heap tuple now... - * - * ... but in POSTGRES, we have no need to do this because VACUUM will - * take care of it later. We can't delete index tuples immediately - * anyway, since the tuple is still visible to other transactions. - */ - /* AFTER ROW DELETE Triggers */ ExecARDeleteTriggers(estate, resultRelInfo, tupleid); @@ -382,10 +444,21 @@ ldelete:; 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 (oldtuple != NULL) + { + deltuple.t_data = oldtuple; + deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple); + ItemPointerSetInvalid(&(deltuple.t_self)); + deltuple.t_tableOid = InvalidOid; + delbuffer = InvalidBuffer; + } + 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,7 +468,8 @@ ldelete:; slot, planSlot); ExecClearTuple(slot); - ReleaseBuffer(delbuffer); + if (BufferIsValid(delbuffer)) + ReleaseBuffer(delbuffer); return rslot; } @@ -413,11 +487,17 @@ ldelete:; * 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, @@ -451,7 +531,7 @@ ExecUpdate(ItemPointer tupleid, /* BEFORE ROW UPDATE Triggers */ if (resultRelInfo->ri_TrigDesc && - resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0) + resultRelInfo->ri_TrigDesc->trig_update_before_row) { HeapTuple newtuple; @@ -480,93 +560,135 @@ ExecUpdate(ItemPointer tupleid, } } - /* - * Check the constraints of the tuple - * - * If we generate a new candidate tuple after EvalPlanQual testing, we - * must loop back here and recheck constraints. (We don't need to redo - * triggers, however. If there are any BEFORE triggers then trigger.c - * will have done heap_lock_tuple to lock the correct tuple, so there's no - * need to do them again.) - */ -lreplace:; - if (resultRelationDesc->rd_att->constr) - ExecConstraints(resultRelInfo, slot, estate); - - /* - * replace the heap tuple - * - * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check that - * the row to be updated is visible to that snapshot, and throw a can't- - * 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) + /* INSTEAD OF ROW UPDATE Triggers */ + if (resultRelInfo->ri_TrigDesc && + resultRelInfo->ri_TrigDesc->trig_update_instead_row) { - case HeapTupleSelfUpdated: - /* already deleted by self; nothing to do */ + HeapTupleData oldtup; + HeapTuple newtuple; + + Assert(oldtuple != NULL); + oldtup.t_data = oldtuple; + oldtup.t_len = HeapTupleHeaderGetDatumLength(oldtuple); + ItemPointerSetInvalid(&(oldtup.t_self)); + oldtup.t_tableOid = InvalidOid; + + newtuple = ExecIRUpdateTriggers(estate, resultRelInfo, + &oldtup, tuple); + + if (newtuple == NULL) /* "do nothing" */ return NULL; - case HeapTupleMayBeUpdated: - break; + 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); - case HeapTupleUpdated: - if (IsolationUsesXactSnapshot()) - ereport(ERROR, - (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("could not serialize access due to concurrent update"))); - if (!ItemPointerEquals(tupleid, &update_ctid)) - { - TupleTableSlot *epqslot; - - epqslot = EvalPlanQual(estate, - epqstate, - resultRelationDesc, - resultRelInfo->ri_RangeTableIndex, - &update_ctid, - update_xmax); - if (!TupIsNull(epqslot)) + if (newslot->tts_tupleDescriptor != tupdesc) + ExecSetSlotDescriptor(newslot, tupdesc); + ExecStoreTuple(newtuple, newslot, InvalidBuffer, false); + slot = newslot; + tuple = newtuple; + } + } + else + { + /* + * Check the constraints of the tuple + * + * If we generate a new candidate tuple after EvalPlanQual testing, we + * must loop back here and recheck constraints. (We don't need to + * redo triggers, however. If there are any BEFORE triggers then + * trigger.c will have done heap_lock_tuple to lock the correct tuple, + * so there's no need to do them again.) + */ +lreplace:; + if (resultRelationDesc->rd_att->constr) + ExecConstraints(resultRelInfo, slot, estate); + + /* + * replace the heap tuple + * + * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check + * that the row to be updated is visible to that snapshot, and throw a + * can't-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: + /* already deleted by self; nothing to do */ + return NULL; + + case HeapTupleMayBeUpdated: + break; + + case HeapTupleUpdated: + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent update"))); + if (!ItemPointerEquals(tupleid, &update_ctid)) { - *tupleid = update_ctid; - slot = ExecFilterJunk(resultRelInfo->ri_junkFilter, epqslot); - tuple = ExecMaterializeSlot(slot); - goto lreplace; + TupleTableSlot *epqslot; + + epqslot = EvalPlanQual(estate, + epqstate, + resultRelationDesc, + resultRelInfo->ri_RangeTableIndex, + &update_ctid, + update_xmax); + if (!TupIsNull(epqslot)) + { + *tupleid = update_ctid; + slot = ExecFilterJunk(resultRelInfo->ri_junkFilter, epqslot); + tuple = ExecMaterializeSlot(slot); + goto lreplace; + } } - } - /* tuple already deleted; nothing to do */ - return NULL; + /* tuple already deleted; nothing to do */ + return NULL; - default: - elog(ERROR, "unrecognized heap_update status: %u", result); - return NULL; - } + default: + elog(ERROR, "unrecognized heap_update status: %u", result); + return NULL; + } - (estate->es_processed)++; + /* + * Note: instead of having to update the old index tuples associated + * with the heap tuple, all we do is form and insert new index + * tuples. This is because UPDATEs are actually DELETEs and INSERTs, + * and index tuple deletion is done later by VACUUM (see notes in + * ExecDelete). All we do here is insert new index tuples. -cim + * 9/27/89 + */ - /* - * Note: instead of having to update the old index tuples associated with - * the heap tuple, all we do is form and insert new index tuples. This is - * because UPDATEs are actually DELETEs and INSERTs, and index tuple - * deletion is done later by VACUUM (see notes in ExecDelete). All we do - * here is insert new index tuples. -cim 9/27/89 - */ + /* + * insert index entries for tuple + * + * Note: heap_update returns the tid (location) of the new tuple in + * the t_self field. + * + * If it's a HOT update, we mustn't insert new index entries. + */ + if (resultRelInfo->ri_NumIndices > 0 && !HeapTupleIsHeapOnly(tuple)) + recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), + estate); + } - /* - * insert index entries for tuple - * - * Note: heap_update returns the tid (location) of the new tuple in the - * t_self field. - * - * If it's a HOT update, we mustn't insert new index entries. - */ - if (resultRelInfo->ri_NumIndices > 0 && !HeapTupleIsHeapOnly(tuple)) - recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), - estate); + (estate->es_processed)++; /* AFTER ROW UPDATE Triggers */ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, @@ -654,6 +776,7 @@ ExecModifyTable(ModifyTableState *node) TupleTableSlot *planSlot; ItemPointer tupleid = NULL; ItemPointerData tuple_ctid; + HeapTupleHeader oldtuple = NULL; /* * On first call, fire BEFORE STATEMENT triggers before proceeding. @@ -714,22 +837,37 @@ ExecModifyTable(ModifyTableState *node) if (junkfilter != NULL) { /* - * extract the 'ctid' junk attribute. + * extract the 'ctid' or 'wholerow' 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; + if (estate->es_result_relation_info->ri_RelationDesc->rd_rel->relkind == RELKIND_RELATION) + { + 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 ctid!! */ + tupleid = &tuple_ctid; + } + else + { + datum = ExecGetJunkAttribute(slot, + junkfilter->jf_junkAttNo, + &isNull); + /* shouldn't ever get a null result... */ + if (isNull) + elog(ERROR, "wholerow is NULL"); + + oldtuple = DatumGetHeapTupleHeader(datum); + } } /* @@ -745,11 +883,11 @@ ExecModifyTable(ModifyTableState *node) slot = ExecInsert(slot, planSlot, estate); break; case CMD_UPDATE: - slot = ExecUpdate(tupleid, slot, planSlot, + slot = ExecUpdate(tupleid, oldtuple, slot, planSlot, &node->mt_epqstate, estate); break; case CMD_DELETE: - slot = ExecDelete(tupleid, planSlot, + slot = ExecDelete(tupleid, oldtuple, planSlot, &node->mt_epqstate, estate); break; default: @@ -934,8 +1072,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) /* * Initialize the junk filter(s) if needed. INSERT queries need a filter * if there are any junk attrs in the tlist. UPDATE and DELETE always - * need a filter, since there's always a junk 'ctid' attribute present --- - * no need to look first. + * need a filter, since there's always a junk 'ctid' or 'wholerow' + * attribute present --- no need to look first. * * If there are multiple result relations, each one needs its own junk * filter. Note multiple rels are only possible for UPDATE/DELETE, so we @@ -988,10 +1126,19 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) 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"); + /* For UPDATE/DELETE, find the appropriate junk attr now */ + if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_RELATION) + { + j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid"); + if (!AttributeNumberIsValid(j->jf_junkAttNo)) + elog(ERROR, "could not find junk ctid column"); + } + else + { + j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow"); + if (!AttributeNumberIsValid(j->jf_junkAttNo)) + elog(ERROR, "could not find junk wholerow column"); + } } resultRelInfo->ri_junkFilter = j; |