*** ./contrib/spi/autoinc.c.orig 2010-07-31 12:06:47.000000000 +0100 --- ./contrib/spi/autoinc.c 2010-08-15 12:55:08.000000000 +0100 *************** *** 38,44 **** if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) /* internal error */ elog(ERROR, "cannot process STATEMENT events"); ! if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) /* internal error */ elog(ERROR, "must be fired before event"); --- 38,44 ---- if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) /* internal error */ elog(ERROR, "cannot process STATEMENT events"); ! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event)) /* internal error */ elog(ERROR, "must be fired before event"); *** ./contrib/spi/insert_username.c.orig 2010-07-31 12:07:44.000000000 +0100 --- ./contrib/spi/insert_username.c 2010-08-15 12:56:59.000000000 +0100 *************** *** 41,47 **** if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) /* internal error */ elog(ERROR, "insert_username: cannot process STATEMENT events"); ! if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) /* internal error */ elog(ERROR, "insert_username: must be fired before event"); --- 41,47 ---- if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) /* internal error */ elog(ERROR, "insert_username: cannot process STATEMENT events"); ! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event)) /* internal error */ elog(ERROR, "insert_username: must be fired before event"); *** ./contrib/spi/moddatetime.c.orig 2010-07-31 12:10:47.000000000 +0100 --- ./contrib/spi/moddatetime.c 2010-08-15 12:57:35.000000000 +0100 *************** *** 47,53 **** /* internal error */ elog(ERROR, "moddatetime: cannot process STATEMENT events"); ! if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) /* internal error */ elog(ERROR, "moddatetime: must be fired before event"); --- 47,53 ---- /* internal error */ elog(ERROR, "moddatetime: cannot process STATEMENT events"); ! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event)) /* internal error */ elog(ERROR, "moddatetime: must be fired before event"); *** ./contrib/spi/timetravel.c.orig 2010-07-31 12:13:01.000000000 +0100 --- ./contrib/spi/timetravel.c 2010-08-15 16:42:48.000000000 +0100 *************** *** 122,128 **** elog(ERROR, "timetravel: cannot process STATEMENT events"); /* Should be called BEFORE */ ! if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) elog(ERROR, "timetravel: must be fired before event"); /* INSERT ? */ --- 122,128 ---- elog(ERROR, "timetravel: cannot process STATEMENT events"); /* Should be called BEFORE */ ! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event)) elog(ERROR, "timetravel: must be fired before event"); /* INSERT ? */ *** ./doc/src/sgml/catalogs.sgml.orig 2010-09-04 14:38:30.000000000 +0100 --- ./doc/src/sgml/catalogs.sgml 2010-09-04 15:04:08.000000000 +0100 *************** *** 4711,4717 **** ! The catalog pg_trigger stores triggers on tables. See for more information. --- 4711,4717 ---- ! The catalog pg_trigger stores triggers on tables and views. See for more information. *** ./doc/src/sgml/information_schema.sgml.orig 2010-08-31 11:57:59.000000000 +0100 --- ./doc/src/sgml/information_schema.sgml 2010-08-31 12:59:42.000000000 +0100 *************** *** 4885,4892 **** The view triggers contains all triggers defined ! in the current database on tables that the current user owns or has ! some non-SELECT privilege on. --- 4885,4892 ---- The view triggers contains all triggers defined ! in the current database on tables and views that the current user owns ! or has some non-SELECT privilege on.
*************** *** 4990,4997 **** condition_timingcharacter_data ! Time at which the trigger fires (BEFORE or ! AFTER) --- 4990,4997 ---- condition_timingcharacter_data ! Time at which the trigger fires (BEFORE, ! AFTER or INSTEAD OF) *************** *** 5562,5580 **** is_trigger_updatable yes_or_no ! Applies to a feature not available in PostgreSQL is_trigger_deletable yes_or_no ! Applies to a feature not available in PostgreSQL is_trigger_insertable_into yes_or_no ! Applies to a feature not available in PostgreSQL --- 5562,5589 ---- is_trigger_updatable yes_or_no ! ! YES if the view has an INSTEAD OF ! UPDATE trigger defined on it, NO if not ! is_trigger_deletable yes_or_no ! ! YES if the view has an INSTEAD OF ! DELETE trigger defined on it, NO if not ! is_trigger_insertable_into yes_or_no ! ! YES if the view has an INSTEAD OF ! INSERT trigger defined on it, NO if not ! *** ./doc/src/sgml/plperl.sgml.orig 2010-09-01 15:41:47.000000000 +0100 --- ./doc/src/sgml/plperl.sgml 2010-09-01 16:51:11.000000000 +0100 *************** *** 980,986 **** $_TD->{when} ! When the trigger was called: BEFORE, AFTER, or UNKNOWN --- 980,988 ---- $_TD->{when} ! When the trigger was called: BEFORE, ! AFTER, INSTEAD OF or ! UNKNOWN *** ./doc/src/sgml/plpgsql.sgml.orig 2010-08-30 20:13:35.000000000 +0100 --- ./doc/src/sgml/plpgsql.sgml 2010-09-04 19:39:00.000000000 +0100 *************** *** 3112,3120 **** TG_WHEN ! Data type text; a string of either ! BEFORE or AFTER ! depending on the trigger's definition. --- 3112,3120 ---- TG_WHEN ! Data type text; a string of ! BEFORE, AFTER or ! INSTEAD OF depending on the trigger's definition. *************** *** 3239,3244 **** --- 3239,3259 ---- + INSTEAD OF triggers (which are always row-level triggers, + and may only be used on views) can return null to signal that they did + not perform any updates, and that the rest of the operation for this + row should be skipped (i.e., subsequent triggers are not fired, and the + INSERT/UPDATE/DELETE did not occur + for this row). Otherwise a nonnull value should be returned, to signal + that the trigger performed the requested operation. For + INSERT and UPDATE operations, the return value + should be NEW, which the trigger function may modify to + support INSERT RETURNING and UPDATE RETURNING. + For DELETE operations, the return value should be + OLD. + + + The return value of a row-level trigger fired AFTER or a statement-level trigger fired BEFORE or AFTER is *************** *** 3354,3359 **** --- 3369,3453 ---- + + A variation of the previous example uses a view joining the main table + to the audit table, to show when each entry was last modified. This + approach still records the full audit trail of changes to the table, + but also presents a simplified view of the audit trail, showing just + the last modified timestamp derived from the audit trail for each entry. + shows an example + of an audit trigger on a view in PL/pgSQL. + + + + A <application>PL/pgSQL</application> View Trigger Procedure For Auditing + + + This example uses a trigger on the view to make it updatable, and + ensure that any insert, update or delete of a row in the view is + recorded (i.e., audited) in the emp_audit table. The current time + and user name are recorded, together with the type of operation + performed, and the view displays the last modified time of each row. + + + + CREATE TABLE emp ( + empname text PRIMARY KEY, + salary integer + ); + + CREATE TABLE emp_audit( + operation char(1) NOT NULL, + userid text NOT NULL, + empname text NOT NULL, + salary integer, + stamp timestamp NOT NULL + ); + + CREATE VIEW emp_view AS + SELECT e.empname, + e.salary, + max(ea.stamp) AS last_updated + FROM emp e + LEFT JOIN emp_audit ea ON ea.empname = e.empname + GROUP BY 1, 2; + + CREATE OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$ + BEGIN + -- + -- Perform the required operation on emp, and create a row in emp_audit + -- to reflect the change made to emp. + -- + IF (TG_OP = 'DELETE') THEN + DELETE FROM emp WHERE empname = OLD.empname; + IF NOT FOUND THEN RETURN NULL; END IF; + + OLD.last_updated = now(); + INSERT INTO emp_audit VALUES('D', user, OLD.*); + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname; + IF NOT FOUND THEN RETURN NULL; END IF; + + NEW.last_updated = now(); + INSERT INTO emp_audit VALUES('U', user, NEW.*); + RETURN NEW; + ELSIF (TG_OP = 'INSERT') THEN + INSERT INTO emp VALUES(NEW.empname, NEW.salary); + + NEW.last_updated = now(); + INSERT INTO emp_audit VALUES('I', user, NEW.*); + RETURN NEW; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER emp_audit + INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view + FOR EACH ROW EXECUTE PROCEDURE update_emp_view(); + + + One use of triggers is to maintain a summary table of another table. The resulting summary can be used in place of the *** ./doc/src/sgml/plpython.sgml.orig 2010-09-01 18:01:23.000000000 +0100 --- ./doc/src/sgml/plpython.sgml 2010-09-01 18:14:17.000000000 +0100 *************** *** 751,757 **** contains one of BEFORE, AFTER, ! or UNKNOWN. --- 751,757 ---- contains one of BEFORE, AFTER, ! INSTEAD OF or UNKNOWN. *************** *** 827,838 **** ! If TD["when"] is BEFORE and TD["level"] is ROW, you can return None or "OK" from the Python function to indicate the row is unmodified, ! "SKIP" to abort the event, or "MODIFY" to ! indicate you've modified the row. Otherwise the return value is ignored. --- 827,840 ---- ! If TD["when"] is BEFORE or ! INSTEAD OF and TD["level"] is ROW, you can return None or "OK" from the Python function to indicate the row is unmodified, ! "SKIP" to abort the event, or if TD["event"] ! is INSERT or UPDATE you can return ! "MODIFY" to indicate you've modified the new row. Otherwise the return value is ignored. *** ./doc/src/sgml/pltcl.sgml.orig 2010-09-01 15:07:44.000000000 +0100 --- ./doc/src/sgml/pltcl.sgml 2010-09-01 17:48:37.000000000 +0100 *************** *** 567,574 **** $TG_when ! The string BEFORE or AFTER depending on the ! type of trigger event. --- 567,574 ---- $TG_when ! The string BEFORE, AFTER or ! INSTEAD OF depending on the type of trigger event. *************** *** 641,650 **** the operation (INSERT/UPDATE/DELETE) that fired the trigger will proceed normally. SKIP tells the trigger manager to silently suppress the operation for this row. If a list is returned, it tells PL/Tcl to ! return a modified row to the trigger manager that will be inserted ! instead of the one given in $NEW. (This works for INSERT and UPDATE ! only.) Needless to say that all this is only meaningful when the trigger ! is BEFORE and FOR EACH ROW; otherwise the return value is ignored. --- 641,654 ---- the operation (INSERT/UPDATE/DELETE) that fired the trigger will proceed normally. SKIP tells the trigger manager to silently suppress the operation for this row. If a list is returned, it tells PL/Tcl to ! return a modified row to the trigger manager. This is only meaningful ! for row-level BEFORE INSERT or UPDATE ! triggers for which the modified row will be inserted instead of the one ! given in $NEW; or for row-level INSTEAD OF ! INSERT or UPDATE triggers where the returned row ! is used to support INSERT RETURNING and ! UPDATE RETURNING commands. The return value is ignored for ! other types of triggers. *** ./doc/src/sgml/ref/create_rule.sgml.orig 2010-09-01 18:44:58.000000000 +0100 --- ./doc/src/sgml/ref/create_rule.sgml 2010-09-01 18:55:21.000000000 +0100 *************** *** 73,79 **** sufficient for your purposes) to replace update actions on the view with appropriate updates on other tables. If you want to support INSERT RETURNING and so on, then be sure to put a suitable ! RETURNING clause into each of these rules. --- 73,81 ---- sufficient for your purposes) to replace update actions on the view with appropriate updates on other tables. If you want to support INSERT RETURNING and so on, then be sure to put a suitable ! RETURNING clause into each of these rules. Alternatively, ! an updatable view may be implemented using INSTEAD OF ! triggers (see ). *** ./doc/src/sgml/ref/create_trigger.sgml.orig 2010-09-01 19:04:12.000000000 +0100 --- ./doc/src/sgml/ref/create_trigger.sgml 2010-09-04 18:14:52.000000000 +0100 *************** *** 21,27 **** ! CREATE TRIGGER name { BEFORE | AFTER } { event [ OR ... ] } ON table [ FOR [ EACH ] { ROW | STATEMENT } ] [ WHEN ( condition ) ] EXECUTE PROCEDURE function_name ( arguments ) --- 21,27 ---- ! CREATE TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] } ON table [ FOR [ EACH ] { ROW | STATEMENT } ] [ WHEN ( condition ) ] EXECUTE PROCEDURE function_name ( arguments ) *************** *** 33,53 **** CREATE TRIGGER creates a new trigger. The ! trigger will be associated with the specified table and will execute the specified function function_name when certain events occur. ! The trigger can be specified to fire either before the operation is attempted on a row (before constraints are checked and the INSERT, UPDATE, or ! DELETE is attempted) or after the operation has completed (after constraints are checked and the INSERT, UPDATE, or ! DELETE has completed). If the trigger fires ! before the event, the trigger can skip the operation for the ! current row, or change the row being inserted (for INSERT and UPDATE operations only). If the trigger fires after the event, all changes, including the effects of other triggers, are visible --- 33,54 ---- CREATE TRIGGER creates a new trigger. The ! trigger will be associated with the specified table or view and will execute the specified function function_name when certain events occur. ! The trigger can be specified to fire before the operation is attempted on a row (before constraints are checked and the INSERT, UPDATE, or ! DELETE is attempted); after the operation has completed (after constraints are checked and the INSERT, UPDATE, or ! DELETE has completed); or instead of the operation ! (in the case of inserts, updates or deletes on a view). ! If the trigger fires before or instead of the event, the trigger can skip ! the operation for the current row, or change the row being inserted (for INSERT and UPDATE operations only). If the trigger fires after the event, all changes, including the effects of other triggers, are visible *************** *** 68,86 **** ! In addition, triggers may be defined to fire for a ! TRUNCATE, though only ! FOR EACH STATEMENT. ! Also, a trigger definition can specify a Boolean WHEN condition, which will be tested to see whether the trigger should be fired. In row-level triggers the WHEN condition can examine the old and/or new values of columns of the row. Statement-level triggers can also have WHEN conditions, although the feature is not so useful for them since the condition cannot refer to any values ! in the table. --- 69,144 ---- ! Triggers that are specified to fire INSTEAD OF the trigger ! event must be marked FOR EACH ROW, and can only be defined ! on views. BEFORE and AFTER triggers on a view ! must be marked as FOR EACH STATEMENT. ! In addition, triggers may be defined to fire for a ! TRUNCATE (tables only), though only ! FOR EACH STATEMENT. The following table ! summarises which types of triggers may be used on tables and views: ! ! ! ! ! ! ! When ! Event ! Row-level ! Statement-level ! ! ! ! ! BEFORE ! INSERT/UPDATE/DELETE ! Tables ! Tables and views ! ! ! TRUNCATE ! X ! Tables ! ! ! AFTER ! INSERT/UPDATE/DELETE ! Tables ! Tables and views ! ! ! TRUNCATE ! X ! Tables ! ! ! INSTEAD OF ! INSERT/UPDATE/DELETE ! Views ! X ! ! ! TRUNCATE ! X ! X ! ! ! ! ! ! ! Also, most trigger definitions can specify a Boolean WHEN condition, which will be tested to see whether the trigger should be fired. In row-level triggers the WHEN condition can examine the old and/or new values of columns of the row. Statement-level triggers can also have WHEN conditions, although the feature is not so useful for them since the condition cannot refer to any values ! in the table. INSTEAD OF triggers cannot be used with ! WHEN conditions. *************** *** 116,125 **** BEFORE AFTER ! Determines whether the function is called before or after the ! event. --- 174,184 ---- BEFORE AFTER + INSTEAD OF ! Determines whether the function is called before, after or instead of ! the event. *************** *** 143,148 **** --- 202,213 ---- The trigger will only fire if at least one of the listed columns is mentioned as a target of the update. + + + INSTEAD OF triggers do not support lists of columns. An + INSTEAD OF trigger must be unconditional, and is fired + for each affected row in the view. + *************** *** 150,156 **** table ! The name (optionally schema-qualified) of the table the trigger is for. --- 215,221 ---- table ! The name (optionally schema-qualified) of the table or view the trigger is for. *************** *** 189,194 **** --- 254,264 ---- + INSTEAD OF triggers do not support WHEN + conditions. + + + Currently, WHEN expressions cannot contain subqueries. *************** *** 326,331 **** --- 396,411 ---- WHEN (OLD.* IS DISTINCT FROM NEW.*) EXECUTE PROCEDURE log_account_update(); + + Execute the function view_insert_row for each row to insert + rows into a view: + + + CREATE TRIGGER view_insert + INSTEAD OF INSERT ON my_view + FOR EACH ROW + EXECUTE PROCEDURE view_insert_row(); + *************** *** 396,402 **** The ability to fire triggers for TRUNCATE is a ! PostgreSQL extension of the SQL standard. --- 476,485 ---- The ability to fire triggers for TRUNCATE is a ! PostgreSQL extension of the SQL standard, as is the ! ability to define statement-level triggers on views that fire ! BEFORE or AFTER INSERT, ! UPDATE and DELETE commands. *** ./doc/src/sgml/rules.sgml.orig 2010-08-28 18:12:42.000000000 +0100 --- ./doc/src/sgml/rules.sgml 2010-08-31 14:02:45.000000000 +0100 *************** *** 168,176 **** DELETE commands don't need a target list because they don't produce any result. In fact, the planner will ! add a special CTID entry to the empty target list, but ! this is after the rule system and will be discussed later; for the ! rule system, the target list is empty. --- 168,178 ---- DELETE commands don't need a target list because they don't produce any result. In fact, the planner will ! add a special CTID entry to the empty target list (if the ! result relation is a table), but this is after the rule system and ! will be discussed later; for the rule system, the target list is empty. ! If the result relation is a view, the rule system will behave slightly ! differently, as described later. *************** *** 189,198 **** For UPDATE commands, the target list describes the new rows that should replace the old ones. In the rule system, it contains just the expressions from the SET ! column = expression part of the command. The planner will handle ! missing columns by inserting expressions that copy the values from ! the old row into the new one. And it will add the special ! CTID entry just as for DELETE, too. --- 191,202 ---- For UPDATE commands, the target list describes the new rows that should replace the old ones. In the rule system, it contains just the expressions from the SET ! column = expression part of the command. The planner will ! handle missing columns by inserting expressions that copy the values ! from the old row into the new one. And it will add the special ! CTID entry, if the result relation is a table, just as ! for DELETE. If the result relation is a view, ! the rule system will behave slightly differently, as described later. *************** *** 656,662 **** Two details of the query tree aren't touched in the description of view rules above. These are the command type and the result relation. ! In fact, view rules don't need this information. --- 660,668 ---- Two details of the query tree aren't touched in the description of view rules above. These are the command type and the result relation. ! In fact, the command type is not needed by view rules, but the result ! relation may affect the way in which the query rewriter works, because ! special care needs to be taken if the result relation is a view. *************** *** 725,745 **** will do. But there is a little problem in ! UPDATE: The executor does not care what the ! results from the join it is doing are meant for. It just produces ! a result set of rows. The difference that one is a ! SELECT command and the other is an ! UPDATE is handled in the caller of the ! executor. The caller still knows (looking at the query tree) that ! this is an UPDATE, and it knows that this ! result should go into table t1. But which of the rows that are ! there has to be replaced by the new row? ! To resolve this problem, another entry is added to the target list ! in UPDATE (and also in ! DELETE) statements: the current tuple ID (CTID).CTID This is a system column containing the file block number and position in the block for the row. Knowing --- 731,750 ---- will do. But there is a little problem in ! UPDATE: The branch of the executor plan that does ! the join does not care what the results from the join it is doing are ! meant for. It just produces a result set of rows. The difference that ! one is a SELECT command and the other is an ! UPDATE is handled higher up in the executor, where ! it knows that this is an UPDATE, and it knows that ! this result should go into table t1. But which of the rows ! that are there has to be replaced by the new row? ! If the result relation is a table, this problem is resolved by adding ! another entry to the target list in UPDATE (and ! also in DELETE) statements: the current tuple ID (CTID).CTID This is a system column containing the file block number and position in the block for the row. Knowing *************** *** 764,770 **** Knowing all that, we can simply apply view rules in absolutely ! the same way to any command. There is no difference. --- 769,777 ---- Knowing all that, we can simply apply view rules in absolutely ! the same way to any command. There is no difference, unless the view is ! the result relation, which must be handled differently (see ! ). *************** *** 803,818 **** What happens if a view is named as the target relation for an INSERT, UPDATE, or ! DELETE? After doing the substitutions ! described above, we will have a query tree in which the result ! relation points at a subquery range-table entry. This will not ! work, so the rewriter throws an error if it sees it has produced ! such a thing. ! To change this, we can define rules that modify the behavior of ! these kinds of commands. This is the topic of the next section. --- 810,859 ---- What happens if a view is named as the target relation for an INSERT, UPDATE, or ! DELETE? Simply doing the substitutions ! described above would give a query tree in which the result ! relation points at a subquery range-table entry, which would not ! work. The rewriter handles this case slightly differently, leaving ! the original view range-table entry as the result relation, and for ! UPDATE and DELETE operations, ! adding a new subquery range-table entry based on the view's query. ! Another problem that now arises is how to identify the rows to be ! updated in the result relation. Recall that when the result relation ! was a table, a special CTID entry was added to the target ! list to identify the physical locations of the rows to be updated. ! This does not work if the result relation is a view, because a view ! typically does not have a CTID, and its rows may not have ! unique physical locations. Instead, for an UPDATE ! or DELETE operation, a special wholerow ! entry is added to the target list, which expands to include all ! columns from the view. The executor does not know how to update the ! view; it relies on INSTEAD OF triggers for this. The ! executor uses the wholerow target list entry to pass the ! data from all the columns in the view to the triggers, to allow them ! to work out what to update. ! ! ! ! If there are no INSTEAD OF triggers to update the view, ! the executor will throw an error, because it cannot automatically ! update a view by itself. To change this, we can define rules that ! modify the behavior of INSERT, ! UPDATE, and DELETE commands on ! a view. These rules will rewrite the command, typically into a command ! that updates one or more tables, rather than views. This is the topic ! of the next section. ! ! ! ! Note that rules are evaluated first, rewriting the original query ! before it is planned and executed. Therefore, if a view has ! INSTEAD OF triggers as well as rules on INSERT, ! UPDATE, and DELETE, then the rules will be ! evaluated first, and depending on the result, the triggers may not be ! used at all. *************** *** 1868,1876 **** ! On the other hand, a trigger cannot be created on views because ! there is no real data in a view relation; however INSERT, UPDATE, ! and DELETE rules can be created on views. --- 1909,1923 ---- ! In this chapter, we focused on using rules to update views. All of ! the update rule examples in this chapter can also be implemented ! using INSTEAD OF triggers on the views (triggers defined ! to fire instead of the view update operations, with trigger functions ! that do the INSERT, UPDATE and ! DELETE operations on the base tables). Writing such ! triggers is often easier than writing rules, particularly if complex ! logic is required to perform the update, however, it is possible that ! for bulk update operations, rules may perform better than triggers. *** ./doc/src/sgml/trigger.sgml.orig 2010-08-28 18:09:13.000000000 +0100 --- ./doc/src/sgml/trigger.sgml 2010-09-01 17:17:41.000000000 +0100 *************** *** 33,39 **** A trigger is a specification that the database should automatically execute a particular function whenever a certain type of operation is ! performed. Triggers can be defined to execute either before or after any INSERT, UPDATE, or DELETE operation, either once per modified row, or once per SQL statement. --- 33,43 ---- A trigger is a specification that the database should automatically execute a particular function whenever a certain type of operation is ! performed. ! ! ! ! On tables, triggers can be defined to execute either before or after any INSERT, UPDATE, or DELETE operation, either once per modified row, or once per SQL statement. *************** *** 46,51 **** --- 50,68 ---- + On views, triggers can be defined to execute instead of + INSERT, UPDATE, or + DELETE operations, and are fired once per row that + needs to be modified in the view. It is the responsibility of the + trigger's function to perform the necessary modifications to the + underlying base tables and, where appropriate, return the modified + row as it appears in the view. Triggers can also be defined to + execute once per SQL statement, before or after + INSERT, UPDATE, or + DELETE operations. + + + The trigger function must be defined before the trigger itself can be created. The trigger function must be declared as a function taking no arguments and returning type trigger. *************** *** 74,91 **** two types of triggers are sometimes called row-level triggers and statement-level triggers, respectively. Triggers on TRUNCATE may only be ! defined at statement-level. ! Triggers are also classified as before triggers and ! after triggers. Statement-level before triggers naturally fire before the statement starts to do anything, while statement-level after ! triggers fire at the very end of the statement. Row-level before triggers fire immediately before a particular row is operated on, while row-level after triggers fire at the end of the statement ! (but before any statement-level after triggers). --- 91,115 ---- two types of triggers are sometimes called row-level triggers and statement-level triggers, respectively. Triggers on TRUNCATE may only be ! defined at statement-level. On views, triggers that fire before or ! after may only be defined at statement-level, and triggers that fire ! instead of an INSERT, UPDATE, ! or DELETE may only be defined at row-level. ! Triggers are also classified as before triggers, ! after triggers or instead of triggers. Statement-level before triggers naturally fire before the statement starts to do anything, while statement-level after ! triggers fire at the very end of the statement. These types of ! triggers may be defined on tables or views. Row-level before triggers fire immediately before a particular row is operated on, while row-level after triggers fire at the end of the statement ! (but before any statement-level after triggers). These types of ! triggers may only be defined on tables. Row-level instead of ! triggers may only be defined on views, and fire immediately as ! each row in the view is identified as needing to be operated on. *************** *** 126,131 **** --- 150,171 ---- + A row-level instead of trigger should either return + NULL to indicate that it did not modify any data from + the view's underlying base tables, or it should return the view + row that was passed in (the NEW row + for INSERT and UPDATE + operations, or the OLD row for + DELETE operations). A nonnull return value is + used to signal that the trigger performed the necessary data + modifications in the view, and will cause the count of the number + of rows affected in the command status to be incremented. For + INSERT and UPDATE operations, the trigger + may modify the NEW row before returning it, to support + INSERT RETURNING and UPDATE RETURNING. + + + The return value is ignored for row-level triggers fired after an operation, and so they can return NULL. *************** *** 133,141 **** If more than one trigger is defined for the same event on the same relation, the triggers will be fired in alphabetical order by ! trigger name. In the case of before triggers, the possibly-modified row returned by each trigger becomes the input ! to the next trigger. If any before trigger returns NULL, the operation is abandoned for that row and subsequent triggers are not fired. --- 173,181 ---- If more than one trigger is defined for the same event on the same relation, the triggers will be fired in alphabetical order by ! trigger name. In the case of before and instead of triggers, the possibly-modified row returned by each trigger becomes the input ! to the next trigger. If any before or instead of trigger returns NULL, the operation is abandoned for that row and subsequent triggers are not fired. *************** *** 157,162 **** --- 197,203 ---- to queue an event nor to re-fetch the row at end of statement. This can result in significant speedups in statements that modify many rows, if the trigger only needs to be fired for a few of the rows. + Instead of triggers on views do not support WHEN conditions. *************** *** 376,381 **** --- 417,431 ---- + + + TRIGGER_FIRED_INSTEAD(tg_event) + + + Returns true if the trigger fired instead of the operation. + + + TRIGGER_FIRED_AFTER(tg_event) *** ./src/backend/catalog/index.c.orig 2010-08-14 23:52:07.000000000 +0100 --- ./src/backend/catalog/index.c 2010-08-15 09:53:21.000000000 +0100 *************** *** 825,830 **** --- 825,831 ---- trigger->funcname = SystemFuncName("unique_key_recheck"); trigger->args = NIL; trigger->before = false; + trigger->instead = false; trigger->row = true; trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE; trigger->columns = NIL; *** ./src/backend/catalog/information_schema.sql.orig 2010-08-28 16:44:27.000000000 +0100 --- ./src/backend/catalog/information_schema.sql 2010-08-31 12:43:19.000000000 +0100 *************** *** 1937,1943 **** CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END AS character_data) AS action_orientation, CAST( ! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' ELSE 'AFTER' END AS character_data) AS condition_timing, CAST(null AS sql_identifier) AS condition_reference_old_table, CAST(null AS sql_identifier) AS condition_reference_new_table, --- 1937,1943 ---- CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END AS character_data) AS action_orientation, CAST( ! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' WHEN t.tgtype & 64 = 64 THEN 'INSTEAD OF' ELSE 'AFTER' END AS character_data) AS condition_timing, CAST(null AS sql_identifier) AS condition_reference_old_table, CAST(null AS sql_identifier) AS condition_reference_new_table, *************** *** 2233,2241 **** THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, ! CAST('NO' AS yes_or_no) AS is_trigger_updatable, ! CAST('NO' AS yes_or_no) AS is_trigger_deletable, ! CAST('NO' AS yes_or_no) AS is_trigger_insertable_into FROM pg_namespace nc, pg_class c --- 2233,2252 ---- THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, ! CAST( ! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 80 = 80) ! THEN 'YES' ELSE 'NO' END ! AS yes_or_no) AS is_trigger_updatable, ! ! CAST( ! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 72 = 72) ! THEN 'YES' ELSE 'NO' END ! AS yes_or_no) AS is_trigger_deletable, ! ! CAST( ! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 68 = 68) ! THEN 'YES' ELSE 'NO' END ! AS yes_or_no) AS is_trigger_insertable_into FROM pg_namespace nc, pg_class c *** ./src/backend/catalog/sql_features.txt.orig 2010-09-04 15:10:12.000000000 +0100 --- ./src/backend/catalog/sql_features.txt 2010-09-04 15:10:42.000000000 +0100 *************** *** 426,432 **** T211 Basic trigger capability 07 TRIGGER privilege YES T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted T212 Enhanced trigger capability YES ! T213 INSTEAD OF triggers NO T231 Sensitive cursors YES T241 START TRANSACTION statement YES T251 SET TRANSACTION statement: LOCAL option NO --- 426,432 ---- T211 Basic trigger capability 07 TRIGGER privilege YES T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted T212 Enhanced trigger capability YES ! T213 INSTEAD OF triggers YES T231 Sensitive cursors YES T241 START TRANSACTION statement YES T251 SET TRANSACTION statement: LOCAL option NO *** ./src/backend/commands/tablecmds.c.orig 2010-08-19 08:50:53.000000000 +0100 --- ./src/backend/commands/tablecmds.c 2010-08-19 08:50:52.000000000 +0100 *************** *** 5619,5624 **** --- 5619,5625 ---- fk_trigger->trigname = "RI_ConstraintTrigger"; fk_trigger->relation = myRel; fk_trigger->before = false; + fk_trigger->instead = false; fk_trigger->row = true; /* Either ON INSERT or ON UPDATE */ *************** *** 5682,5687 **** --- 5683,5689 ---- fk_trigger->trigname = "RI_ConstraintTrigger"; fk_trigger->relation = fkconstraint->pktable; fk_trigger->before = false; + fk_trigger->instead = false; fk_trigger->row = true; fk_trigger->events = TRIGGER_TYPE_DELETE; fk_trigger->columns = NIL; *************** *** 5735,5740 **** --- 5737,5743 ---- fk_trigger->trigname = "RI_ConstraintTrigger"; fk_trigger->relation = fkconstraint->pktable; fk_trigger->before = false; + fk_trigger->instead = false; fk_trigger->row = true; fk_trigger->events = TRIGGER_TYPE_UPDATE; fk_trigger->columns = NIL; *** ./src/backend/commands/trigger.c.orig 2010-08-28 17:15:25.000000000 +0100 --- ./src/backend/commands/trigger.c 2010-09-01 16:58:12.000000000 +0100 *************** *** 150,159 **** */ rel = heap_openrv(stmt->relation, ShareRowExclusiveLock); ! if (rel->rd_rel->relkind != RELKIND_RELATION) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), ! errmsg("\"%s\" is not a table", RelationGetRelationName(rel)))); if (!allowSystemTableMods && IsSystemRelation(rel)) --- 150,198 ---- */ rel = heap_openrv(stmt->relation, ShareRowExclusiveLock); ! if (rel->rd_rel->relkind == RELKIND_RELATION) ! { ! /* Forbid INSTEAD OF triggers on tables */ ! if (stmt->instead) ! ereport(ERROR, ! (errcode(ERRCODE_WRONG_OBJECT_TYPE), ! errmsg("\"%s\" is a table", ! RelationGetRelationName(rel)), ! errhint("Define INSTEAD OF triggers on views"))); ! } ! else if (rel->rd_rel->relkind == RELKIND_VIEW) ! { ! /* Only allow INSTEAD OF or STATEMENT-level triggers on views */ ! if (!stmt->instead && stmt->row) ! ereport(ERROR, ! (errcode(ERRCODE_WRONG_OBJECT_TYPE), ! errmsg("\"%s\" is a view", ! RelationGetRelationName(rel)), ! errhint("Define ROW-level BEFORE and AFTER triggers on tables"))); ! ! /* Disallow STATEMENT-level INSTEAD OF triggers */ ! if (stmt->instead && !stmt->row) ! ereport(ERROR, ! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ! errmsg("STATEMENT-level INSTEAD OF triggers are not supported"), ! errhint("Use FOR EACH ROW for INSTEAD OF triggers"))); ! ! /* Disallow WHEN clauses with INSTEAD OF triggers */ ! if (stmt->instead && stmt->whenClause) ! ereport(ERROR, ! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ! errmsg("cannot use a WHEN condition with an INSTEAD OF trigger"))); ! ! /* Disallow column selection with INSTEAD OF triggers */ ! if (stmt->instead && stmt->columns != NIL) ! ereport(ERROR, ! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ! errmsg("cannot use a column list with an INSTEAD OF trigger"))); ! } ! else ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), ! errmsg("\"%s\" is not a table or view", RelationGetRelationName(rel)))); if (!allowSystemTableMods && IsSystemRelation(rel)) *************** *** 188,193 **** --- 227,234 ---- TRIGGER_CLEAR_TYPE(tgtype); if (stmt->before) TRIGGER_SETT_BEFORE(tgtype); + if (stmt->instead) + TRIGGER_SETT_INSTEAD(tgtype); if (stmt->row) TRIGGER_SETT_ROW(tgtype); tgtype |= stmt->events; *************** *** 198,203 **** --- 239,252 ---- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("TRUNCATE FOR EACH ROW triggers are not supported"))); + /* Disallow TRUNCATE triggers on VIEWs */ + if (rel->rd_rel->relkind == RELKIND_VIEW && TRIGGER_FOR_TRUNCATE(tgtype)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a view", + RelationGetRelationName(rel)), + errhint("Define TRUNCATE triggers on tables"))); + /* * Parse the WHEN clause, if any */ *************** *** 1031,1040 **** rel = heap_open(relid, ShareRowExclusiveLock); ! if (rel->rd_rel->relkind != RELKIND_RELATION) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), ! errmsg("\"%s\" is not a table", RelationGetRelationName(rel)))); if (!allowSystemTableMods && IsSystemRelation(rel)) --- 1080,1090 ---- rel = heap_open(relid, ShareRowExclusiveLock); ! if (rel->rd_rel->relkind != RELKIND_RELATION && ! rel->rd_rel->relkind != RELKIND_VIEW) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), ! errmsg("\"%s\" is not a table or view", RelationGetRelationName(rel)))); if (!allowSystemTableMods && IsSystemRelation(rel)) *************** *** 1506,1511 **** --- 1556,1566 ---- n = trigdesc->n_before_row; t = trigdesc->tg_before_row; } + else if (TRIGGER_FOR_INSTEAD(trigger->tgtype)) + { + n = trigdesc->n_instead_row; + t = trigdesc->tg_instead_row; + } else { n = trigdesc->n_after_row; *************** *** 1656,1661 **** --- 1711,1729 ---- else t[i] = NULL; } + n = newdesc->n_instead_row; + t = newdesc->tg_instead_row; + for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++) + { + if (n[i] > 0) + { + tnew = (int *) palloc(n[i] * sizeof(int)); + memcpy(tnew, t[i], n[i] * sizeof(int)); + t[i] = tnew; + } + else + t[i] = NULL; + } n = newdesc->n_after_statement; t = newdesc->tg_after_statement; for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++) *************** *** 1698,1703 **** --- 1766,1775 ---- for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++) if (t[i] != NULL) pfree(t[i]); + t = trigdesc->tg_instead_row; + for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++) + if (t[i] != NULL) + pfree(t[i]); t = trigdesc->tg_after_statement; for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++) if (t[i] != NULL) *************** *** 2000,2005 **** --- 2072,2121 ---- true, NULL, trigtuple, recheckIndexes, NULL); } + HeapTuple + ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo, + HeapTuple trigtuple) + { + TriggerDesc *trigdesc = relinfo->ri_TrigDesc; + int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_INSERT]; + int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_INSERT]; + TriggerData LocTriggerData; + HeapTuple newtuple = trigtuple; + HeapTuple rettuple; + int i; + + LocTriggerData.type = T_TriggerData; + LocTriggerData.tg_event = TRIGGER_EVENT_INSERT | + TRIGGER_EVENT_ROW | + TRIGGER_EVENT_INSTEAD; + LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + for (i = 0; i < ntrigs; i++) + { + Trigger *trigger = &trigdesc->triggers[tgindx[i]]; + + if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event, + NULL, NULL, trigtuple)) + continue; + + LocTriggerData.tg_trigtuple = newtuple; + LocTriggerData.tg_trigtuplebuf = InvalidBuffer; + LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_newtuplebuf = InvalidBuffer; + LocTriggerData.tg_trigger = trigger; + rettuple = ExecCallTriggerFunc(&LocTriggerData, + tgindx[i], + relinfo->ri_TrigFunctions, + relinfo->ri_TrigInstrument, + GetPerTupleMemoryContext(estate)); + if (rettuple != newtuple && newtuple != trigtuple) + heap_freetuple(newtuple); + newtuple = rettuple; + if (newtuple == NULL) + break; + } + return newtuple; + } + void ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo) { *************** *** 2134,2139 **** --- 2250,2297 ---- } } + bool + ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, + HeapTuple trigtuple) + { + TriggerDesc *trigdesc = relinfo->ri_TrigDesc; + int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_DELETE]; + int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_DELETE]; + TriggerData LocTriggerData; + HeapTuple rettuple; + int i; + + LocTriggerData.type = T_TriggerData; + LocTriggerData.tg_event = TRIGGER_EVENT_DELETE | + TRIGGER_EVENT_ROW | + TRIGGER_EVENT_INSTEAD; + LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + for (i = 0; i < ntrigs; i++) + { + Trigger *trigger = &trigdesc->triggers[tgindx[i]]; + + if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event, + NULL, trigtuple, NULL)) + continue; + + LocTriggerData.tg_trigtuple = trigtuple; + LocTriggerData.tg_trigtuplebuf = InvalidBuffer; + LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_newtuplebuf = InvalidBuffer; + LocTriggerData.tg_trigger = trigger; + rettuple = ExecCallTriggerFunc(&LocTriggerData, + tgindx[i], + relinfo->ri_TrigFunctions, + relinfo->ri_TrigInstrument, + GetPerTupleMemoryContext(estate)); + if (rettuple == NULL) + return false; /* Delete was suppressed */ + if (rettuple != trigtuple) + heap_freetuple(rettuple); + } + return true; + } + void ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo) { *************** *** 2281,2286 **** --- 2439,2488 ---- } } + HeapTuple + ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, + HeapTuple oldtuple, HeapTuple newtuple) + { + TriggerDesc *trigdesc = relinfo->ri_TrigDesc; + int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_UPDATE]; + int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_UPDATE]; + TriggerData LocTriggerData; + HeapTuple intuple = newtuple; + HeapTuple rettuple; + int i; + + LocTriggerData.type = T_TriggerData; + LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE | + TRIGGER_EVENT_ROW | + TRIGGER_EVENT_INSTEAD; + LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + for (i = 0; i < ntrigs; i++) + { + Trigger *trigger = &trigdesc->triggers[tgindx[i]]; + + if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event, + NULL, oldtuple, newtuple)) + continue; + + LocTriggerData.tg_trigtuple = oldtuple; + LocTriggerData.tg_newtuple = newtuple; + LocTriggerData.tg_trigtuplebuf = InvalidBuffer; + LocTriggerData.tg_newtuplebuf = InvalidBuffer; + LocTriggerData.tg_trigger = trigger; + rettuple = ExecCallTriggerFunc(&LocTriggerData, + tgindx[i], + relinfo->ri_TrigFunctions, + relinfo->ri_TrigInstrument, + GetPerTupleMemoryContext(estate)); + if (rettuple != newtuple && newtuple != intuple) + heap_freetuple(newtuple); + newtuple = rettuple; + if (newtuple == NULL) + break; + } + return newtuple; + } + void ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo) { *** ./src/backend/executor/execMain.c.orig 2010-08-15 00:04:31.000000000 +0100 --- ./src/backend/executor/execMain.c 2010-08-15 10:01:37.000000000 +0100 *************** *** 899,908 **** RelationGetRelationName(resultRelationDesc)))); break; case RELKIND_VIEW: ! ereport(ERROR, ! (errcode(ERRCODE_WRONG_OBJECT_TYPE), ! errmsg("cannot change view \"%s\"", ! RelationGetRelationName(resultRelationDesc)))); break; default: ereport(ERROR, --- 899,905 ---- RelationGetRelationName(resultRelationDesc)))); break; case RELKIND_VIEW: ! /* OK */ break; default: ereport(ERROR, *** ./src/backend/executor/nodeModifyTable.c.orig 2010-08-19 08:53:10.000000000 +0100 --- ./src/backend/executor/nodeModifyTable.c 2010-09-04 22:53:39.000000000 +0100 *************** *** 149,154 **** --- 149,200 ---- } /* ---------------------------------------------------------------- + * ExecCheckViewTriggers + * + * If the result relation is a view, check that it has the + * appropriate INSTEAD OF triggers to carry out the current + * operation. + * ---------------------------------------------------------------- + */ + static void + ExecCheckViewTriggers(ResultRelInfo *resultRelInfo, + CmdType operation) + { + TriggerDesc *trigDesc = resultRelInfo->ri_TrigDesc; + + switch (operation) + { + case CMD_INSERT: + if (!trigDesc || + trigDesc->n_instead_row[TRIGGER_EVENT_INSERT] == 0) + ereport(ERROR, + (errmsg("cannot insert into a view"), + errhint("You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger."))); + break; + + case CMD_UPDATE: + if (!trigDesc || + trigDesc->n_instead_row[TRIGGER_EVENT_UPDATE] == 0) + ereport(ERROR, + (errmsg("cannot update a view"), + errhint("You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger."))); + break; + + case CMD_DELETE: + if (!trigDesc || + trigDesc->n_instead_row[TRIGGER_EVENT_DELETE] == 0) + ereport(ERROR, + (errmsg("cannot delete from a view"), + errhint("You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger."))); + break; + + default: + elog(ERROR, "unknown operation"); + break; + } + } + + /* ---------------------------------------------------------------- * ExecInsert * * For INSERT, we have to insert the tuple into the target relation *************** *** 167,172 **** --- 213,219 ---- Relation resultRelationDesc; Oid newId; List *recheckIndexes = NIL; + bool resultRelIsView; /* * get the heap tuple out of the tuple table slot, making sure we have a *************** *** 179,184 **** --- 226,232 ---- */ resultRelInfo = estate->es_result_relation_info; resultRelationDesc = resultRelInfo->ri_RelationDesc; + resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW; /* * If the result relation has OIDs, force the tuple's OID to zero so that *************** *** 195,202 **** if (resultRelationDesc->rd_rel->relhasoids) HeapTupleSetOid(tuple, InvalidOid); ! /* BEFORE ROW INSERT Triggers */ ! if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0) { HeapTuple newtuple; --- 243,251 ---- if (resultRelationDesc->rd_rel->relhasoids) HeapTupleSetOid(tuple, InvalidOid); ! /* BEFORE ROW INSERT Triggers (tables only) */ ! if (!resultRelIsView && ! resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0) { HeapTuple newtuple; *************** *** 237,244 **** * Note: heap_insert returns the tid (location) of the new tuple in the * t_self field. */ ! newId = heap_insert(resultRelationDesc, tuple, ! estate->es_output_cid, 0, NULL); (estate->es_processed)++; estate->es_lastoid = newId; --- 286,329 ---- * Note: heap_insert returns the tid (location) of the new tuple in the * t_self field. */ ! if (resultRelIsView) ! { ! /* ! * INSTEAD OF trigger(s) do the actual insert ! */ ! HeapTuple newtuple; ! ! ExecCheckViewTriggers(resultRelInfo, CMD_INSERT); ! newtuple = ExecIRInsertTriggers(estate, resultRelInfo, tuple); ! ! if (newtuple == NULL) /* trigger(s) did nothing */ ! return NULL; ! ! if (newtuple != tuple) /* modified by Trigger(s) */ ! { ! /* ! * Put the modified tuple into a slot for convenience of ! * routines below. We assume the tuple was allocated in ! * per-tuple memory context, and therefore will go away by ! * itself. The tuple table slot should not try to clear it. ! */ ! TupleTableSlot *newslot = estate->es_trig_tuple_slot; ! TupleDesc tupdesc = RelationGetDescr(resultRelationDesc); ! ! if (newslot->tts_tupleDescriptor != tupdesc) ! ExecSetSlotDescriptor(newslot, tupdesc); ! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false); ! slot = newslot; ! tuple = newtuple; ! } ! ! newId = InvalidOid; ! } ! else ! { ! newId = heap_insert(resultRelationDesc, tuple, ! estate->es_output_cid, 0, NULL); ! } (estate->es_processed)++; estate->es_lastoid = newId; *************** *** 251,258 **** recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), estate); ! /* AFTER ROW INSERT Triggers */ ! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes); list_free(recheckIndexes); --- 336,344 ---- recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), estate); ! /* AFTER ROW INSERT Triggers (tables only) */ ! if (!resultRelIsView) ! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes); list_free(recheckIndexes); *************** *** 268,280 **** * ExecDelete * * DELETE is like UPDATE, except that we delete the tuple and no ! * index modifications are needed * * Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */ static TupleTableSlot * ExecDelete(ItemPointer tupleid, TupleTableSlot *planSlot, EPQState *epqstate, EState *estate) --- 354,372 ---- * ExecDelete * * DELETE is like UPDATE, except that we delete the tuple and no ! * index modifications are needed. ! * ! * When deleting from a table, tupleid identifies the tuple to ! * delete and oldtuple is NULL. When deleting from a view, ! * oldtuple is passed to the INSTEAD OF triggers and identifies ! * what to delete, and tupleid is invalid. * * Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */ static TupleTableSlot * ExecDelete(ItemPointer tupleid, + HeapTupleHeader oldtuple, TupleTableSlot *planSlot, EPQState *epqstate, EState *estate) *************** *** 284,298 **** HTSU_Result result; ItemPointerData update_ctid; TransactionId update_xmax; /* * get information on the (current) result relation */ resultRelInfo = estate->es_result_relation_info; resultRelationDesc = resultRelInfo->ri_RelationDesc; ! /* BEFORE ROW DELETE Triggers */ ! if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0) { bool dodelete; --- 376,393 ---- HTSU_Result result; ItemPointerData update_ctid; TransactionId update_xmax; + bool resultRelIsView; /* * get information on the (current) result relation */ resultRelInfo = estate->es_result_relation_info; resultRelationDesc = resultRelInfo->ri_RelationDesc; + resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW; ! /* BEFORE ROW DELETE Triggers (tables only) */ ! if (!resultRelIsView && ! resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0) { bool dodelete; *************** *** 313,323 **** * referential integrity updates in serializable transactions. */ ldelete:; ! result = heap_delete(resultRelationDesc, tupleid, ! &update_ctid, &update_xmax, ! estate->es_output_cid, ! estate->es_crosscheck_snapshot, ! true /* wait for commit */ ); switch (result) { case HeapTupleSelfUpdated: --- 408,443 ---- * referential integrity updates in serializable transactions. */ ldelete:; ! if (resultRelIsView) ! { ! /* ! * INSTEAD OF trigger(s) do the actual delete ! */ ! HeapTupleData tuple; ! bool diddelete; ! ! tuple.t_data = oldtuple; ! tuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple); ! ItemPointerSetInvalid(&(tuple.t_self)); ! tuple.t_tableOid = InvalidOid; ! ! ExecCheckViewTriggers(resultRelInfo, CMD_DELETE); ! diddelete = ExecIRDeleteTriggers(estate, resultRelInfo, &tuple); ! ! if (!diddelete) /* trigger(s) did nothing */ ! return NULL; ! ! result = HeapTupleMayBeUpdated; ! } ! else ! { ! result = heap_delete(resultRelationDesc, tupleid, ! &update_ctid, &update_xmax, ! estate->es_output_cid, ! estate->es_crosscheck_snapshot, ! true /* wait for commit */ ); ! } ! switch (result) { case HeapTupleSelfUpdated: *************** *** 367,374 **** * anyway, since the tuple is still visible to other transactions. */ ! /* AFTER ROW DELETE Triggers */ ! ExecARDeleteTriggers(estate, resultRelInfo, tupleid); /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) --- 487,495 ---- * anyway, since the tuple is still visible to other transactions. */ ! /* AFTER ROW DELETE Triggers (tables only) */ ! if (!resultRelIsView) ! ExecARDeleteTriggers(estate, resultRelInfo, tupleid); /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) *************** *** 382,391 **** HeapTupleData deltuple; Buffer delbuffer; ! deltuple.t_self = *tupleid; ! if (!heap_fetch(resultRelationDesc, SnapshotAny, ! &deltuple, &delbuffer, false, NULL)) ! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING"); if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc)) ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc)); --- 503,522 ---- HeapTupleData deltuple; Buffer delbuffer; ! if (resultRelIsView) ! { ! deltuple.t_data = oldtuple; ! deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple); ! ItemPointerSetInvalid(&(deltuple.t_self)); ! deltuple.t_tableOid = InvalidOid; ! } ! else ! { ! deltuple.t_self = *tupleid; ! if (!heap_fetch(resultRelationDesc, SnapshotAny, ! &deltuple, &delbuffer, false, NULL)) ! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING"); ! } if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc)) ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc)); *************** *** 395,401 **** slot, planSlot); ExecClearTuple(slot); ! ReleaseBuffer(delbuffer); return rslot; } --- 526,533 ---- slot, planSlot); ExecClearTuple(slot); ! if (!resultRelIsView) ! ReleaseBuffer(delbuffer); return rslot; } *************** *** 413,423 **** --- 545,561 ---- * is, we don't want to get stuck in an infinite loop * which corrupts your database.. * + * When updating a table, tupleid identifies the tuple to + * update and oldtuple is NULL. When updating a view, oldtuple + * is passed to the INSTEAD OF triggers and identifies what to + * update, and tupleid is invalid. + * * Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */ static TupleTableSlot * ExecUpdate(ItemPointer tupleid, + HeapTupleHeader oldtuple, TupleTableSlot *slot, TupleTableSlot *planSlot, EPQState *epqstate, *************** *** 430,435 **** --- 568,574 ---- ItemPointerData update_ctid; TransactionId update_xmax; List *recheckIndexes = NIL; + bool resultRelIsView; /* * abort the operation if not running transactions *************** *** 448,456 **** */ resultRelInfo = estate->es_result_relation_info; resultRelationDesc = resultRelInfo->ri_RelationDesc; ! /* BEFORE ROW UPDATE Triggers */ ! if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0) { HeapTuple newtuple; --- 587,597 ---- */ resultRelInfo = estate->es_result_relation_info; resultRelationDesc = resultRelInfo->ri_RelationDesc; + resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW; ! /* BEFORE ROW UPDATE Triggers (tables only) */ ! if (!resultRelIsView && ! resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0) { HeapTuple newtuple; *************** *** 501,511 **** * serialize error if not. This is a special-case behavior needed for * referential integrity updates in serializable transactions. */ ! result = heap_update(resultRelationDesc, tupleid, tuple, ! &update_ctid, &update_xmax, ! estate->es_output_cid, ! estate->es_crosscheck_snapshot, ! true /* wait for commit */ ); switch (result) { case HeapTupleSelfUpdated: --- 642,696 ---- * serialize error if not. This is a special-case behavior needed for * referential integrity updates in serializable transactions. */ ! if (resultRelIsView) ! { ! /* ! * INSTEAD OF trigger(s) do the actual update ! */ ! HeapTupleData oldtup; ! HeapTuple newtuple; ! ! oldtup.t_data = oldtuple; ! oldtup.t_len = HeapTupleHeaderGetDatumLength(oldtuple); ! ItemPointerSetInvalid(&(oldtup.t_self)); ! oldtup.t_tableOid = InvalidOid; ! ! ExecCheckViewTriggers(resultRelInfo, CMD_UPDATE); ! newtuple = ExecIRUpdateTriggers(estate, resultRelInfo, ! &oldtup, tuple); ! ! if (newtuple == NULL) /* trigger(s) did nothing */ ! return NULL; ! ! if (newtuple != tuple) /* modified by Trigger(s) */ ! { ! /* ! * Put the modified tuple into a slot for convenience of ! * routines below. We assume the tuple was allocated in ! * per-tuple memory context, and therefore will go away by ! * itself. The tuple table slot should not try to clear it. ! */ ! TupleTableSlot *newslot = estate->es_trig_tuple_slot; ! TupleDesc tupdesc = RelationGetDescr(resultRelationDesc); ! ! if (newslot->tts_tupleDescriptor != tupdesc) ! ExecSetSlotDescriptor(newslot, tupdesc); ! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false); ! slot = newslot; ! tuple = newtuple; ! } ! ! result = HeapTupleMayBeUpdated; ! } ! else ! { ! result = heap_update(resultRelationDesc, tupleid, tuple, ! &update_ctid, &update_xmax, ! estate->es_output_cid, ! estate->es_crosscheck_snapshot, ! true /* wait for commit */ ); ! } ! switch (result) { case HeapTupleSelfUpdated: *************** *** 568,576 **** recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), estate); ! /* AFTER ROW UPDATE Triggers */ ! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, ! recheckIndexes); list_free(recheckIndexes); --- 753,762 ---- recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), estate); ! /* AFTER ROW UPDATE Triggers (tables only) */ ! if (!resultRelIsView) ! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, ! recheckIndexes); list_free(recheckIndexes); *************** *** 654,659 **** --- 840,846 ---- TupleTableSlot *planSlot; ItemPointer tupleid = NULL; ItemPointerData tuple_ctid; + HeapTupleHeader oldtuple; /* * On first call, fire BEFORE STATEMENT triggers before proceeding. *************** *** 713,735 **** if (junkfilter != NULL) { /* ! * extract the 'ctid' junk attribute. */ if (operation == CMD_UPDATE || operation == CMD_DELETE) { Datum datum; bool isNull; ! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo, ! &isNull); ! /* shouldn't ever get a null result... */ ! if (isNull) ! elog(ERROR, "ctid is NULL"); ! ! tupleid = (ItemPointer) DatumGetPointer(datum); ! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */ ! tupleid = &tuple_ctid; } /* --- 900,936 ---- if (junkfilter != NULL) { + ResultRelInfo *resultRelInfo = estate->es_result_relation_info; + Relation resultRel = resultRelInfo->ri_RelationDesc; + /* ! * extract the 'ctid' or 'wholerow' junk attribute. */ if (operation == CMD_UPDATE || operation == CMD_DELETE) { Datum datum; bool isNull; ! if (resultRel->rd_rel->relkind == RELKIND_VIEW) ! { ! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo, ! &isNull); ! if (isNull) ! elog(ERROR, "wholerow is NULL"); ! ! oldtuple = DatumGetHeapTupleHeader(datum); ! } ! else ! { ! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo, ! &isNull); ! if (isNull) ! elog(ERROR, "ctid is NULL"); ! ! tupleid = (ItemPointer) DatumGetPointer(datum); ! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */ ! tupleid = &tuple_ctid; ! } } /* *************** *** 745,755 **** slot = ExecInsert(slot, planSlot, estate); break; case CMD_UPDATE: ! slot = ExecUpdate(tupleid, slot, planSlot, &node->mt_epqstate, estate); break; case CMD_DELETE: ! slot = ExecDelete(tupleid, planSlot, &node->mt_epqstate, estate); break; default: --- 946,956 ---- slot = ExecInsert(slot, planSlot, estate); break; case CMD_UPDATE: ! slot = ExecUpdate(tupleid, oldtuple, slot, planSlot, &node->mt_epqstate, estate); break; case CMD_DELETE: ! slot = ExecDelete(tupleid, oldtuple, planSlot, &node->mt_epqstate, estate); break; default: *************** *** 833,838 **** --- 1034,1043 ---- i = 0; foreach(l, node->plans) { + Relation resultRel = estate->es_result_relation_info->ri_RelationDesc; + if (resultRel->rd_rel->relkind == RELKIND_VIEW) + ExecCheckViewTriggers(estate->es_result_relation_info, operation); + subplan = (Plan *) lfirst(l); mtstate->mt_plans[i] = ExecInitNode(subplan, estate, eflags); estate->es_result_relation_info++; *************** *** 988,997 **** if (operation == CMD_UPDATE || operation == CMD_DELETE) { ! /* For UPDATE/DELETE, find the ctid junk attr now */ ! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid"); ! if (!AttributeNumberIsValid(j->jf_junkAttNo)) ! elog(ERROR, "could not find junk ctid column"); } resultRelInfo->ri_junkFilter = j; --- 1193,1216 ---- if (operation == CMD_UPDATE || operation == CMD_DELETE) { ! /* ! * For UPDATE/DELETE of real tables, find the ctid junk ! * attr now. For VIEWs use the wholerow junk attr. ! */ ! Relation resultRel = resultRelInfo->ri_RelationDesc; ! ! if (resultRel->rd_rel->relkind == RELKIND_VIEW) ! { ! j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow"); ! if (!AttributeNumberIsValid(j->jf_junkAttNo)) ! elog(ERROR, "could not find junk wholerow column"); ! } ! else ! { ! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid"); ! if (!AttributeNumberIsValid(j->jf_junkAttNo)) ! elog(ERROR, "could not find junk ctid column"); ! } } resultRelInfo->ri_junkFilter = j; *** ./src/backend/nodes/copyfuncs.c.orig 2010-08-19 08:55:28.000000000 +0100 --- ./src/backend/nodes/copyfuncs.c 2010-08-19 08:55:27.000000000 +0100 *************** *** 3229,3234 **** --- 3229,3235 ---- COPY_NODE_FIELD(funcname); COPY_NODE_FIELD(args); COPY_SCALAR_FIELD(before); + COPY_SCALAR_FIELD(instead); COPY_SCALAR_FIELD(row); COPY_SCALAR_FIELD(events); COPY_NODE_FIELD(columns); *** ./src/backend/nodes/equalfuncs.c.orig 2010-08-19 08:56:51.000000000 +0100 --- ./src/backend/nodes/equalfuncs.c 2010-08-19 08:56:50.000000000 +0100 *************** *** 1686,1691 **** --- 1686,1692 ---- COMPARE_NODE_FIELD(funcname); COMPARE_NODE_FIELD(args); COMPARE_SCALAR_FIELD(before); + COMPARE_SCALAR_FIELD(instead); COMPARE_SCALAR_FIELD(row); COMPARE_SCALAR_FIELD(events); COMPARE_NODE_FIELD(columns); *** ./src/backend/optimizer/plan/planmain.c.orig 2010-07-28 09:37:12.000000000 +0100 --- ./src/backend/optimizer/plan/planmain.c 2010-08-15 09:29:08.000000000 +0100 *************** *** 180,185 **** --- 180,194 ---- add_base_rels_to_query(root, (Node *) parse->jointree); /* + * If the query target is a VIEW, it won't be in the jointree, but we + * need a dummy RelOptInfo node for it. This need not have any stats in + * it because it always just goes at the top of the plan tree. + */ + if (parse->resultRelation && + root->simple_rel_array[parse->resultRelation] == NULL) + build_simple_rel(root, parse->resultRelation, RELOPT_OTHER_MEMBER_REL); + + /* * Examine the targetlist and qualifications, adding entries to baserel * targetlists for all referenced Vars. Restrict and join clauses are * added to appropriate lists belonging to the mentioned relations. We *** ./src/backend/optimizer/prep/preptlist.c.orig 2010-07-28 09:37:12.000000000 +0100 --- ./src/backend/optimizer/prep/preptlist.c 2010-08-15 09:35:45.000000000 +0100 *************** *** 38,44 **** static List *expand_targetlist(List *tlist, int command_type, ! Index result_relation, List *range_table); /* --- 38,44 ---- static List *expand_targetlist(List *tlist, int command_type, ! Index result_relation, Relation rel); /* *************** *** 52,57 **** --- 52,58 ---- { Query *parse = root->parse; int result_relation = parse->resultRelation; + bool result_is_view = false; List *range_table = parse->rtable; CmdType command_type = parse->commandType; ListCell *lc; *************** *** 63,81 **** if (result_relation) { RangeTblEntry *rte = rt_fetch(result_relation, range_table); if (rte->subquery != NULL || rte->relid == InvalidOid) elog(ERROR, "subquery cannot be result relation"); } /* ! * for heap_form_tuple to work, the targetlist must match the exact order ! * of the attributes. We also need to fill in any missing attributes. -ay ! * 10/94 */ ! if (command_type == CMD_INSERT || command_type == CMD_UPDATE) ! tlist = expand_targetlist(tlist, command_type, ! result_relation, range_table); /* * for "update" and "delete" queries, add ctid of the result relation into --- 64,100 ---- if (result_relation) { RangeTblEntry *rte = rt_fetch(result_relation, range_table); + Relation rel; if (rte->subquery != NULL || rte->relid == InvalidOid) elog(ERROR, "subquery cannot be result relation"); + + /* + * Open the result relation. We assume that the rewriter already + * acquired at least AccessShareLock on it, so we need no lock here. + */ + rel = heap_open(getrelid(result_relation, range_table), NoLock); + result_is_view = rel->rd_rel->relkind == RELKIND_VIEW; + + /* + * for heap_form_tuple to work, the targetlist must match the exact + * order of the attributes. We also need to fill in any missing + * attributes. -ay 10/94 + */ + if (command_type == CMD_INSERT || command_type == CMD_UPDATE) + tlist = expand_targetlist(tlist, command_type, + result_relation, rel); + + heap_close(rel, NoLock); } /* ! * For an UPDATE, expand_targetlist already created a fresh tlist. For ! * DELETE, better do a listCopy so that we don't destructively modify ! * the original tlist (is this really necessary?). */ ! if (command_type == CMD_DELETE) ! tlist = list_copy(tlist); /* * for "update" and "delete" queries, add ctid of the result relation into *************** *** 83,90 **** * ExecutePlan() will be able to identify the right tuple to replace or * delete. This extra field is marked "junk" so that it is not stored * back into the tuple. */ ! if (command_type == CMD_UPDATE || command_type == CMD_DELETE) { TargetEntry *tle; Var *var; --- 102,114 ---- * ExecutePlan() will be able to identify the right tuple to replace or * delete. This extra field is marked "junk" so that it is not stored * back into the tuple. + * + * We don't do this if the result relation is a view, since that won't + * expose a ctid. The rewriter should have already added a wholerow TLE + * for the view's subselect. */ ! if (!result_is_view && ! (command_type == CMD_UPDATE || command_type == CMD_DELETE)) { TargetEntry *tle; Var *var; *************** *** 97,110 **** pstrdup("ctid"), true); - /* - * For an UPDATE, expand_targetlist already created a fresh tlist. For - * DELETE, better do a listCopy so that we don't destructively modify - * the original tlist (is this really necessary?). - */ - if (command_type == CMD_DELETE) - tlist = list_copy(tlist); - tlist = lappend(tlist, tle); } --- 121,126 ---- *************** *** 169,187 **** } else { ! /* Not a table, so we need the whole row as a junk var */ ! var = makeVar(rc->rti, ! InvalidAttrNumber, ! RECORDOID, ! -1, ! 0); ! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti); ! tle = makeTargetEntry((Expr *) var, ! list_length(tlist) + 1, ! pstrdup(resname), ! true); ! tlist = lappend(tlist, tle); ! rc->wholeAttNo = tle->resno; } } --- 185,227 ---- } else { ! bool exists = false; ! ListCell *l; ! ! /* ! * Not a table, so we need the whole row as a junk var. If the ! * query target is a view, the rewriter will have already added ! * a whole row var, so don't add another for that RTE. ! */ ! foreach(l, tlist) ! { ! TargetEntry *tle = (TargetEntry *) lfirst(l); ! ! if (tle->resjunk && ! IsA(tle->expr, Var) && ! ((Var *) tle->expr)->varno == rc->rti && ! strcmp(tle->resname, "wholerow") == 0) ! { ! exists = true; ! break; ! } ! } ! ! if (!exists) ! { ! var = makeVar(rc->rti, ! InvalidAttrNumber, ! RECORDOID, ! -1, ! 0); ! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti); ! tle = makeTargetEntry((Expr *) var, ! list_length(tlist) + 1, ! pstrdup(resname), ! true); ! tlist = lappend(tlist, tle); ! rc->wholeAttNo = tle->resno; ! } } } *************** *** 241,251 **** */ static List * expand_targetlist(List *tlist, int command_type, ! Index result_relation, List *range_table) { List *new_tlist = NIL; ListCell *tlist_item; - Relation rel; int attrno, numattrs; --- 281,290 ---- */ static List * expand_targetlist(List *tlist, int command_type, ! Index result_relation, Relation rel) { List *new_tlist = NIL; ListCell *tlist_item; int attrno, numattrs; *************** *** 256,267 **** * order; but we have to insert TLEs for any missing attributes. * * Scan the tuple description in the relation's relcache entry to make ! * sure we have all the user attributes in the right order. We assume ! * that the rewriter already acquired at least AccessShareLock on the ! * relation, so we need no lock here. */ - rel = heap_open(getrelid(result_relation, range_table), NoLock); - numattrs = RelationGetNumberOfAttributes(rel); for (attrno = 1; attrno <= numattrs; attrno++) --- 295,302 ---- * order; but we have to insert TLEs for any missing attributes. * * Scan the tuple description in the relation's relcache entry to make ! * sure we have all the user attributes in the right order. */ numattrs = RelationGetNumberOfAttributes(rel); for (attrno = 1; attrno <= numattrs; attrno++) *************** *** 399,406 **** tlist_item = lnext(tlist_item); } - heap_close(rel, NoLock); - return new_tlist; } --- 434,439 ---- *** ./src/backend/optimizer/util/plancat.c.orig 2010-08-28 17:18:36.000000000 +0100 --- ./src/backend/optimizer/util/plancat.c 2010-08-28 17:18:35.000000000 +0100 *************** *** 441,447 **** *tuples = 1; break; default: ! /* else it has no disk storage; probably shouldn't get here? */ *pages = 0; *tuples = 0; break; --- 441,452 ---- *tuples = 1; break; default: ! /* ! * Else it has no disk storage; probably shouldn't get here, ! * unless this is a VIEW which is the query target, in which ! * case we don't care about the sizes, since it will always be ! * at the top of the plan tree. ! */ *pages = 0; *tuples = 0; break; *** ./src/backend/parser/gram.y.orig 2010-08-28 17:26:46.000000000 +0100 --- ./src/backend/parser/gram.y 2010-08-28 17:26:44.000000000 +0100 *************** *** 245,251 **** %type 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 *************** *** 3377,3383 **** n->relation = $7; n->funcname = $12; n->args = $14; ! n->before = $4; n->row = $8; n->events = intVal(linitial($5)); n->columns = (List *) lsecond($5); --- 3378,3385 ---- n->relation = $7; n->funcname = $12; n->args = $14; ! n->before = ($4 & 1) != 0; ! n->instead = ($4 & 2) != 0; n->row = $8; n->events = intVal(linitial($5)); n->columns = (List *) lsecond($5); *************** *** 3399,3404 **** --- 3401,3407 ---- n->funcname = $17; n->args = $19; n->before = FALSE; + n->instead = FALSE; n->row = TRUE; n->events = intVal(linitial($6)); n->columns = (List *) lsecond($6); *************** *** 3412,3419 **** ; TriggerActionTime: ! BEFORE { $$ = TRUE; } ! | AFTER { $$ = FALSE; } ; TriggerEvents: --- 3415,3423 ---- ; TriggerActionTime: ! BEFORE { $$ = 1; } ! | AFTER { $$ = 0; } ! | INSTEAD OF { $$ = 2; } ; TriggerEvents: *** ./src/backend/rewrite/rewriteHandler.c.orig 2010-07-28 09:37:27.000000000 +0100 --- ./src/backend/rewrite/rewriteHandler.c 2010-08-15 09:07:20.000000000 +0100 *************** *** 564,570 **** * planner will later insert NULLs for them, but there's no reason to slow * down rewriter processing with extra tlist nodes.) Also, for both INSERT * and UPDATE, replace explicit DEFAULT specifications with column default ! * expressions. * * 2. Merge multiple entries for the same target attribute, or declare error * if we can't. Multiple entries are only allowed for INSERT/UPDATE of --- 564,572 ---- * planner will later insert NULLs for them, but there's no reason to slow * down rewriter processing with extra tlist nodes.) Also, for both INSERT * and UPDATE, replace explicit DEFAULT specifications with column default ! * expressions. For an UPDATE on a VIEW, add tlist entries assigning any ! * unassigned attributes their current values. The RHS of such assignments ! * will be rewritten to refer to the view's subselect node. * * 2. Merge multiple entries for the same target attribute, or declare error * if we can't. Multiple entries are only allowed for INSERT/UPDATE of *************** *** 724,729 **** --- 726,753 ---- false); } + /* + * For an UPDATE on a VIEW where the TLE is missing, assign the + * current value, which will be rewritten as a query from the view's + * subselect node by ApplyRetrieveRule(). + */ + if (new_tle == NULL && commandType == CMD_UPDATE && + target_relation->rd_rel->relkind == RELKIND_VIEW) + { + Node *new_expr; + + new_expr = (Node *) makeVar(parsetree->resultRelation, + attrno, + att_tup->atttypid, + att_tup->atttypmod, + 0); + + new_tle = makeTargetEntry((Expr *) new_expr, + attrno, + pstrdup(NameStr(att_tup->attname)), + false); + } + if (new_tle) new_tlist = lappend(new_tlist, new_tle); } *************** *** 1149,1154 **** --- 1173,1180 ---- RangeTblEntry *rte, *subrte; RowMarkClause *rc; + Var *var; + TargetEntry *tle; if (list_length(rule->actions) != 1) elog(ERROR, "expected just one rule action"); *************** *** 1177,1220 **** */ rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown); ! /* ! * VIEWs are really easy --- just plug the view query in as a subselect, ! * replacing the relation's original RTE. ! */ ! rte = rt_fetch(rt_index, parsetree->rtable); ! rte->rtekind = RTE_SUBQUERY; ! rte->relid = InvalidOid; ! rte->subquery = rule_action; ! rte->inh = false; /* must not be set for a subquery */ ! /* ! * We move the view's permission check data down to its rangetable. The ! * checks will actually be done against the OLD entry therein. ! */ ! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable); ! Assert(subrte->relid == relation->rd_id); ! subrte->requiredPerms = rte->requiredPerms; ! subrte->checkAsUser = rte->checkAsUser; ! subrte->selectedCols = rte->selectedCols; ! subrte->modifiedCols = rte->modifiedCols; ! ! rte->requiredPerms = 0; /* no permission check on subquery itself */ ! rte->checkAsUser = InvalidOid; ! rte->selectedCols = NULL; ! rte->modifiedCols = NULL; ! /* ! * If FOR UPDATE/SHARE of view, mark all the contained tables as implicit ! * FOR UPDATE/SHARE, the same as the parser would have done if the view's ! * subquery had been written out explicitly. ! * ! * Note: we don't consider forUpdatePushedDown here; such marks will be ! * made by recursing from the upper level in markQueryForLocking. ! */ ! if (rc != NULL) ! markQueryForLocking(rule_action, (Node *) rule_action->jointree, ! rc->forUpdate, rc->noWait, true); return parsetree; } --- 1203,1313 ---- */ rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown); ! if (rt_index == parsetree->resultRelation) ! { ! /* ! * We have a VIEW as the query target. For DELETE and UPDATE, we ! * add a new subselect RTE using the view's query and adjust any ! * VARs in the original query to point to this instead of the ! * original view RTE. ! * ! * We keep the original view in the rtable as the query target, and ! * any Vars in the returning list that reference it are left alone. ! * ! * The resulting jointree fromlist will not refer to the view RTE, ! * and so the planner won't try to join to it. This will result in ! * a plan with a ModifyTable node at the root, referring to the ! * original view relation, and a subselect based on the view's query ! * merged with any user conditions. ! * ! * For INSERTS we do nothing. The original view remains the query ! * target. ! */ ! if (parsetree->commandType == CMD_DELETE || ! parsetree->commandType == CMD_UPDATE) ! { ! List *returningList = parsetree->returningList; ! /* ! * Make a new subselect RTE from the view query and adjust the ! * the original query to point to this instead of the original ! * view, while preserving the view resultRelation and any ! * returningList Vars. ! */ ! rte = copyObject(rt_fetch(rt_index, parsetree->rtable)); ! rte->rtekind = RTE_SUBQUERY; ! rte->relid = InvalidOid; ! rte->subquery = rule_action; ! rte->inh = false; /* must not be set for a subquery */ ! parsetree->rtable = lappend(parsetree->rtable, rte); ! parsetree->returningList = NIL; ! ! ChangeVarNodes((Node *) parsetree, rt_index, ! list_length(parsetree->rtable), 0); ! ! parsetree->resultRelation = rt_index; ! parsetree->returningList = returningList; ! ! /* ! * Add a "wholerow" junk TLE so that the executor can retrieve ! * the old VIEW tuples to pass to the INSTEAD OF triggers. ! */ ! var = makeVar(list_length(parsetree->rtable), ! InvalidAttrNumber, ! RECORDOID, ! -1, ! 0); ! ! tle = makeTargetEntry((Expr *) var, ! list_length(parsetree->targetList) + 1, ! pstrdup("wholerow"), ! true); ! parsetree->targetList = lappend(parsetree->targetList, tle); ! } ! } ! else ! { ! /* ! * Selecting from the VIEW --- just plug the view query in as a ! * subselect, replacing the relation's original RTE. ! */ ! rte = rt_fetch(rt_index, parsetree->rtable); ! ! rte->rtekind = RTE_SUBQUERY; ! rte->relid = InvalidOid; ! rte->subquery = rule_action; ! rte->inh = false; /* must not be set for a subquery */ ! ! /* ! * We move the view's permission check data down to its rangetable. ! * The checks will actually be done against the OLD entry therein. ! */ ! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable); ! Assert(subrte->relid == relation->rd_id); ! subrte->requiredPerms = rte->requiredPerms; ! subrte->checkAsUser = rte->checkAsUser; ! subrte->selectedCols = rte->selectedCols; ! subrte->modifiedCols = rte->modifiedCols; ! ! rte->requiredPerms = 0; /* no permission check on subquery itself */ ! rte->checkAsUser = InvalidOid; ! rte->selectedCols = NULL; ! rte->modifiedCols = NULL; ! ! /* ! * If FOR UPDATE/SHARE of view, mark all the contained tables as ! * implicit FOR UPDATE/SHARE, the same as the parser would have done ! * if the view's subquery had been written out explicitly. ! * ! * Note: we don't consider forUpdatePushedDown here; such marks will ! * be made by recursing from the upper level in markQueryForLocking. ! */ ! if (rc != NULL) ! markQueryForLocking(rule_action, (Node *) rule_action->jointree, ! rc->forUpdate, rc->noWait, true); ! } return parsetree; } *** ./src/backend/utils/adt/ruleutils.c.orig 2010-08-15 00:14:30.000000000 +0100 --- ./src/backend/utils/adt/ruleutils.c 2010-08-15 10:45:04.000000000 +0100 *************** *** 545,550 **** --- 545,552 ---- if (TRIGGER_FOR_BEFORE(trigrec->tgtype)) appendStringInfo(&buf, "BEFORE"); + else if (TRIGGER_FOR_INSTEAD(trigrec->tgtype)) + appendStringInfo(&buf, "INSTEAD OF"); else appendStringInfo(&buf, "AFTER"); if (TRIGGER_FOR_INSERT(trigrec->tgtype)) *** ./src/backend/utils/adt/tsvector_op.c.orig 2010-08-15 00:18:37.000000000 +0100 --- ./src/backend/utils/adt/tsvector_op.c 2010-08-15 12:50:47.000000000 +0100 *************** *** 1259,1265 **** trigdata = (TriggerData *) fcinfo->context; if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events"); ! if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event"); if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) --- 1259,1265 ---- trigdata = (TriggerData *) fcinfo->context; if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events"); ! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event)) elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event"); if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) *** ./src/bin/pg_dump/pg_dump.c.orig 2010-08-15 00:21:49.000000000 +0100 --- ./src/bin/pg_dump/pg_dump.c 2010-08-15 10:52:30.000000000 +0100 *************** *** 11745,11750 **** --- 11745,11752 ---- findx = 0; if (TRIGGER_FOR_BEFORE(tginfo->tgtype)) appendPQExpBuffer(query, "BEFORE"); + if (TRIGGER_FOR_INSTEAD(tginfo->tgtype)) + appendPQExpBuffer(query, "INSTEAD OF"); else appendPQExpBuffer(query, "AFTER"); if (TRIGGER_FOR_INSERT(tginfo->tgtype)) *** ./src/bin/psql/describe.c.orig 2010-08-03 17:16:15.000000000 +0100 --- ./src/bin/psql/describe.c 2010-08-15 10:50:29.000000000 +0100 *************** *** 1820,1827 **** --- 1820,1838 ---- } PQclear(result); } + } + /* + * Print triggers next. + * This is part of the footer information about a table, but may also + * apply to a view. + */ + if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v') + { /* print triggers (but only user-defined triggers) */ + PGresult *result = NULL; + int tuples = 0; + if (tableinfo.hastriggers) { printfPQExpBuffer(&buf, *************** *** 1935,1942 **** --- 1946,1963 ---- } PQclear(result); } + } + /* + * Done printing trigger information for view or table. + * Finish printing the footer information about a table. + */ + if (tableinfo.relkind == 'r') + { /* print inherited tables */ + PGresult *result = NULL; + int tuples = 0; + printfPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i WHERE c.oid=i.inhparent AND i.inhrelid = '%s' ORDER BY inhseqno", oid); result = PSQLexec(buf.data, false); *** ./src/include/catalog/pg_trigger.h.orig 2010-09-04 15:00:15.000000000 +0100 --- ./src/include/catalog/pg_trigger.h 2010-09-04 15:16:04.000000000 +0100 *************** *** 91,96 **** --- 91,97 ---- #define TRIGGER_TYPE_DELETE (1 << 3) #define TRIGGER_TYPE_UPDATE (1 << 4) #define TRIGGER_TYPE_TRUNCATE (1 << 5) + #define TRIGGER_TYPE_INSTEAD (1 << 6) /* Macros for manipulating tgtype */ #define TRIGGER_CLEAR_TYPE(type) ((type) = 0) *************** *** 101,106 **** --- 102,108 ---- #define TRIGGER_SETT_DELETE(type) ((type) |= TRIGGER_TYPE_DELETE) #define TRIGGER_SETT_UPDATE(type) ((type) |= TRIGGER_TYPE_UPDATE) #define TRIGGER_SETT_TRUNCATE(type) ((type) |= TRIGGER_TYPE_TRUNCATE) + #define TRIGGER_SETT_INSTEAD(type) ((type) |= TRIGGER_TYPE_INSTEAD) #define TRIGGER_FOR_ROW(type) ((type) & TRIGGER_TYPE_ROW) #define TRIGGER_FOR_BEFORE(type) ((type) & TRIGGER_TYPE_BEFORE) *************** *** 108,112 **** --- 110,115 ---- #define TRIGGER_FOR_DELETE(type) ((type) & TRIGGER_TYPE_DELETE) #define TRIGGER_FOR_UPDATE(type) ((type) & TRIGGER_TYPE_UPDATE) #define TRIGGER_FOR_TRUNCATE(type) ((type) & TRIGGER_TYPE_TRUNCATE) + #define TRIGGER_FOR_INSTEAD(type) ((type) & TRIGGER_TYPE_INSTEAD) #endif /* PG_TRIGGER_H */ *** ./src/include/commands/trigger.h.orig 2010-08-15 00:24:40.000000000 +0100 --- ./src/include/commands/trigger.h 2010-09-01 16:59:34.000000000 +0100 *************** *** 53,58 **** --- 53,59 ---- #define TRIGGER_EVENT_OPMASK 0x00000003 #define TRIGGER_EVENT_ROW 0x00000004 #define TRIGGER_EVENT_BEFORE 0x00000008 + #define TRIGGER_EVENT_INSTEAD 0x00000040 /* More TriggerEvent flags, used only within trigger.c */ *************** *** 84,91 **** #define TRIGGER_FIRED_BEFORE(event) \ ((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE) #define TRIGGER_FIRED_AFTER(event) \ ! (!TRIGGER_FIRED_BEFORE (event)) /* * Definitions for the replication role based firing. --- 85,95 ---- #define TRIGGER_FIRED_BEFORE(event) \ ((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE) + #define TRIGGER_FIRED_INSTEAD(event) \ + ((TriggerEvent) (event) & TRIGGER_EVENT_INSTEAD) + #define TRIGGER_FIRED_AFTER(event) \ ! (!TRIGGER_FIRED_BEFORE (event) && !TRIGGER_FIRED_INSTEAD (event)) /* * Definitions for the replication role based firing. *************** *** 135,140 **** --- 139,147 ---- ResultRelInfo *relinfo, HeapTuple trigtuple, List *recheckIndexes); + extern HeapTuple ExecIRInsertTriggers(EState *estate, + ResultRelInfo *relinfo, + HeapTuple trigtuple); extern void ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo); extern void ExecASDeleteTriggers(EState *estate, *************** *** 146,151 **** --- 153,161 ---- extern void ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid); + extern bool ExecIRDeleteTriggers(EState *estate, + ResultRelInfo *relinfo, + HeapTuple trigtuple); extern void ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo); extern void ExecASUpdateTriggers(EState *estate, *************** *** 160,165 **** --- 170,179 ---- ItemPointer tupleid, HeapTuple newtuple, List *recheckIndexes); + extern HeapTuple ExecIRUpdateTriggers(EState *estate, + ResultRelInfo *relinfo, + HeapTuple oldtuple, + HeapTuple newtuple); extern void ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo); extern void ExecASTruncateTriggers(EState *estate, *** ./src/include/nodes/parsenodes.h.orig 2010-08-19 08:59:46.000000000 +0100 --- ./src/include/nodes/parsenodes.h 2010-08-19 08:59:45.000000000 +0100 *************** *** 1608,1613 **** --- 1608,1614 ---- List *funcname; /* qual. name of function to call */ List *args; /* list of (T_String) Values or NIL */ bool before; /* BEFORE/AFTER */ + bool instead; /* INSTEAD OF (overrides BEFORE/AFTER) */ bool row; /* ROW/STATEMENT */ /* events uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */ int16 events; /* INSERT/UPDATE/DELETE/TRUNCATE */ *** ./src/include/utils/rel.h.orig 2010-08-15 00:30:33.000000000 +0100 --- ./src/include/utils/rel.h 2010-08-15 10:20:43.000000000 +0100 *************** *** 82,91 **** --- 82,93 ---- uint16 n_before_statement[TRIGGER_NUM_EVENT_CLASSES]; uint16 n_before_row[TRIGGER_NUM_EVENT_CLASSES]; uint16 n_after_row[TRIGGER_NUM_EVENT_CLASSES]; + uint16 n_instead_row[TRIGGER_NUM_EVENT_CLASSES]; uint16 n_after_statement[TRIGGER_NUM_EVENT_CLASSES]; int *tg_before_statement[TRIGGER_NUM_EVENT_CLASSES]; int *tg_before_row[TRIGGER_NUM_EVENT_CLASSES]; int *tg_after_row[TRIGGER_NUM_EVENT_CLASSES]; + int *tg_instead_row[TRIGGER_NUM_EVENT_CLASSES]; int *tg_after_statement[TRIGGER_NUM_EVENT_CLASSES]; /* The actual array of triggers is here */ *** ./src/pl/plperl/expected/plperl_trigger.out.orig 2010-09-04 14:58:26.000000000 +0100 --- ./src/pl/plperl/expected/plperl_trigger.out 2010-09-04 14:57:10.000000000 +0100 *************** *** 124,129 **** --- 124,207 ---- CONTEXT: PL/Perl function "trigger_data" DROP TRIGGER show_trigger_data_trig on trigger_test; + insert into trigger_test values(1,'insert'); + CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test; + CREATE TRIGGER show_trigger_data_trig + INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view + FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view'); + insert into trigger_test_view values(2,'insert'); + NOTICE: $_TD->{argc} = '2' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{args} = ['24', 'skidoo view'] + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{event} = 'INSERT' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{level} = 'ROW' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{name} = 'show_trigger_data_trig' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{new} = {'i' => '2', 'v' => 'insert'} + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{relid} = 'bogus:12345' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{relname} = 'trigger_test_view' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{table_name} = 'trigger_test_view' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{table_schema} = 'public' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{when} = 'INSTEAD OF' + CONTEXT: PL/Perl function "trigger_data" + update trigger_test_view set v = 'update' where i = 1; + NOTICE: $_TD->{argc} = '2' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{args} = ['24', 'skidoo view'] + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{event} = 'UPDATE' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{level} = 'ROW' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{name} = 'show_trigger_data_trig' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{new} = {'i' => '1', 'v' => 'update'} + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'} + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{relid} = 'bogus:12345' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{relname} = 'trigger_test_view' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{table_name} = 'trigger_test_view' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{table_schema} = 'public' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{when} = 'INSTEAD OF' + CONTEXT: PL/Perl function "trigger_data" + delete from trigger_test_view; + NOTICE: $_TD->{argc} = '2' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{args} = ['24', 'skidoo view'] + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{event} = 'DELETE' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{level} = 'ROW' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{name} = 'show_trigger_data_trig' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'} + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{relid} = 'bogus:12345' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{relname} = 'trigger_test_view' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{table_name} = 'trigger_test_view' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{table_schema} = 'public' + CONTEXT: PL/Perl function "trigger_data" + NOTICE: $_TD->{when} = 'INSTEAD OF' + CONTEXT: PL/Perl function "trigger_data" + DROP VIEW trigger_test_view; + delete from trigger_test; DROP FUNCTION trigger_data(); CREATE OR REPLACE FUNCTION valid_id() RETURNS trigger AS $$ *** ./src/pl/plperl/plperl.c.orig 2010-07-31 12:19:30.000000000 +0100 --- ./src/pl/plperl/plperl.c 2010-08-30 23:39:05.000000000 +0100 *************** *** 954,959 **** --- 954,961 ---- when = "BEFORE"; else if (TRIGGER_FIRED_AFTER(tdata->tg_event)) when = "AFTER"; + else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event)) + when = "INSTEAD OF"; else when = "UNKNOWN"; hv_store_string(hv, "when", newSVstring(when)); *** ./src/pl/plperl/sql/plperl_trigger.sql.orig 2010-09-04 10:57:46.000000000 +0100 --- ./src/pl/plperl/sql/plperl_trigger.sql 2010-09-04 11:13:40.000000000 +0100 *************** *** 60,65 **** --- 60,79 ---- delete from trigger_test; DROP TRIGGER show_trigger_data_trig on trigger_test; + + insert into trigger_test values(1,'insert'); + CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test; + + CREATE TRIGGER show_trigger_data_trig + INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view + FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view'); + + insert into trigger_test_view values(2,'insert'); + update trigger_test_view set v = 'update' where i = 1; + delete from trigger_test_view; + + DROP VIEW trigger_test_view; + delete from trigger_test; DROP FUNCTION trigger_data(); *** ./src/pl/plpgsql/src/pl_exec.c.orig 2010-08-28 17:29:02.000000000 +0100 --- ./src/pl/plpgsql/src/pl_exec.c 2010-08-30 23:37:49.000000000 +0100 *************** *** 581,588 **** var->value = CStringGetTextDatum("BEFORE"); else if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) var->value = CStringGetTextDatum("AFTER"); else ! elog(ERROR, "unrecognized trigger execution time: not BEFORE or AFTER"); var->isnull = false; var->freeval = true; --- 581,590 ---- var->value = CStringGetTextDatum("BEFORE"); else if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) var->value = CStringGetTextDatum("AFTER"); + else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event)) + var->value = CStringGetTextDatum("INSTEAD OF"); else ! elog(ERROR, "unrecognized trigger execution time: not BEFORE, AFTER or INSTEAD OF"); var->isnull = false; var->freeval = true; *** ./src/pl/plpython/expected/plpython_trigger.out.orig 2010-09-04 11:22:40.000000000 +0100 --- ./src/pl/plpython/expected/plpython_trigger.out 2010-09-04 11:34:21.000000000 +0100 *************** *** 294,304 **** CONTEXT: PL/Python function "trigger_data" NOTICE: TD[when] => BEFORE CONTEXT: PL/Python function "trigger_data" DROP FUNCTION trigger_data() CASCADE; ! NOTICE: drop cascades to 3 other objects ! DETAIL: drop cascades to trigger show_trigger_data_trig_before on table trigger_test ! drop cascades to trigger show_trigger_data_trig_after on table trigger_test ! drop cascades to trigger show_trigger_data_trig_stmt on table trigger_test -- -- trigger error handling -- --- 294,374 ---- CONTEXT: PL/Python function "trigger_data" NOTICE: TD[when] => BEFORE CONTEXT: PL/Python function "trigger_data" + DROP TRIGGER show_trigger_data_trig_stmt on trigger_test; + DROP TRIGGER show_trigger_data_trig_before on trigger_test; + DROP TRIGGER show_trigger_data_trig_after on trigger_test; + insert into trigger_test values(1,'insert'); + CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test; + CREATE TRIGGER show_trigger_data_trig + INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view + FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view'); + insert into trigger_test_view values(2,'insert'); + NOTICE: TD[args] => ['24', 'skidoo view'] + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[event] => INSERT + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[level] => ROW + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[name] => show_trigger_data_trig + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[new] => {'i': 2, 'v': 'insert'} + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[old] => None + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[relid] => bogus:12345 + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[table_name] => trigger_test_view + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[table_schema] => public + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[when] => INSTEAD OF + CONTEXT: PL/Python function "trigger_data" + update trigger_test_view set v = 'update' where i = 1; + NOTICE: TD[args] => ['24', 'skidoo view'] + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[event] => UPDATE + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[level] => ROW + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[name] => show_trigger_data_trig + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[new] => {'i': 1, 'v': 'update'} + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[old] => {'i': 1, 'v': 'insert'} + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[relid] => bogus:12345 + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[table_name] => trigger_test_view + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[table_schema] => public + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[when] => INSTEAD OF + CONTEXT: PL/Python function "trigger_data" + delete from trigger_test_view; + NOTICE: TD[args] => ['24', 'skidoo view'] + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[event] => DELETE + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[level] => ROW + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[name] => show_trigger_data_trig + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[new] => None + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[old] => {'i': 1, 'v': 'insert'} + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[relid] => bogus:12345 + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[table_name] => trigger_test_view + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[table_schema] => public + CONTEXT: PL/Python function "trigger_data" + NOTICE: TD[when] => INSTEAD OF + CONTEXT: PL/Python function "trigger_data" DROP FUNCTION trigger_data() CASCADE; ! NOTICE: drop cascades to trigger show_trigger_data_trig on view trigger_test_view ! DROP VIEW trigger_test_view; ! delete from trigger_test; -- -- trigger error handling -- *** ./src/pl/plpython/plpython.c.orig 2010-08-28 17:30:36.000000000 +0100 --- ./src/pl/plpython/plpython.c 2010-08-30 23:39:52.000000000 +0100 *************** *** 846,851 **** --- 846,853 ---- pltwhen = PyString_FromString("BEFORE"); else if (TRIGGER_FIRED_AFTER(tdata->tg_event)) pltwhen = PyString_FromString("AFTER"); + else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event)) + pltwhen = PyString_FromString("INSTEAD OF"); else { elog(ERROR, "unrecognized WHEN tg_event: %u", tdata->tg_event); *** ./src/pl/plpython/sql/plpython_trigger.sql.orig 2010-09-04 11:22:22.000000000 +0100 --- ./src/pl/plpython/sql/plpython_trigger.sql 2010-09-04 11:33:54.000000000 +0100 *************** *** 99,106 **** delete from trigger_test; truncate table trigger_test; ! DROP FUNCTION trigger_data() CASCADE; -- -- trigger error handling --- 99,122 ---- delete from trigger_test; truncate table trigger_test; ! DROP TRIGGER show_trigger_data_trig_stmt on trigger_test; ! DROP TRIGGER show_trigger_data_trig_before on trigger_test; ! DROP TRIGGER show_trigger_data_trig_after on trigger_test; ! ! insert into trigger_test values(1,'insert'); ! CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test; ! ! CREATE TRIGGER show_trigger_data_trig ! INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view ! FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view'); + insert into trigger_test_view values(2,'insert'); + update trigger_test_view set v = 'update' where i = 1; + delete from trigger_test_view; + + DROP FUNCTION trigger_data() CASCADE; + DROP VIEW trigger_test_view; + delete from trigger_test; -- -- trigger error handling *** ./src/pl/tcl/expected/pltcl_queries_1.out.orig 2010-09-04 11:45:57.000000000 +0100 --- ./src/pl/tcl/expected/pltcl_queries_1.out 2010-09-04 22:06:23.000000000 +0100 *************** *** 196,201 **** --- 196,237 ---- NOTICE: TG_table_schema: public NOTICE: TG_when: BEFORE NOTICE: args: {23 skidoo} + insert into trigger_test_view values(2,'insert'); + NOTICE: NEW: {i: 2, v: insert} + NOTICE: OLD: {} + NOTICE: TG_level: ROW + NOTICE: TG_name: show_trigger_data_view_trig + NOTICE: TG_op: INSERT + NOTICE: TG_relatts: {{} i v} + NOTICE: TG_relid: bogus:12345 + NOTICE: TG_table_name: trigger_test_view + NOTICE: TG_table_schema: public + NOTICE: TG_when: {INSTEAD OF} + NOTICE: args: {24 {skidoo view}} + update trigger_test_view set v = 'update' where i=1; + NOTICE: NEW: {i: 1, v: update} + NOTICE: OLD: {i: 1, v: insert} + NOTICE: TG_level: ROW + NOTICE: TG_name: show_trigger_data_view_trig + NOTICE: TG_op: UPDATE + NOTICE: TG_relatts: {{} i v} + NOTICE: TG_relid: bogus:12345 + NOTICE: TG_table_name: trigger_test_view + NOTICE: TG_table_schema: public + NOTICE: TG_when: {INSTEAD OF} + NOTICE: args: {24 {skidoo view}} + delete from trigger_test_view; + NOTICE: NEW: {} + NOTICE: OLD: {i: 1, v: insert} + NOTICE: TG_level: ROW + NOTICE: TG_name: show_trigger_data_view_trig + NOTICE: TG_op: DELETE + NOTICE: TG_relatts: {{} i v} + NOTICE: TG_relid: bogus:12345 + NOTICE: TG_table_name: trigger_test_view + NOTICE: TG_table_schema: public + NOTICE: TG_when: {INSTEAD OF} + NOTICE: args: {24 {skidoo view}} update trigger_test set v = 'update' where i = 1; NOTICE: NEW: {i: 1, v: update} NOTICE: OLD: {i: 1, v: insert} *** ./src/pl/tcl/expected/pltcl_queries.out.orig 2010-09-04 11:38:26.000000000 +0100 --- ./src/pl/tcl/expected/pltcl_queries.out 2010-09-04 22:05:50.000000000 +0100 *************** *** 196,201 **** --- 196,237 ---- NOTICE: TG_table_schema: public NOTICE: TG_when: BEFORE NOTICE: args: {23 skidoo} + insert into trigger_test_view values(2,'insert'); + NOTICE: NEW: {i: 2, v: insert} + NOTICE: OLD: {} + NOTICE: TG_level: ROW + NOTICE: TG_name: show_trigger_data_view_trig + NOTICE: TG_op: INSERT + NOTICE: TG_relatts: {{} i v} + NOTICE: TG_relid: bogus:12345 + NOTICE: TG_table_name: trigger_test_view + NOTICE: TG_table_schema: public + NOTICE: TG_when: {INSTEAD OF} + NOTICE: args: {24 {skidoo view}} + update trigger_test_view set v = 'update' where i=1; + NOTICE: NEW: {i: 1, v: update} + NOTICE: OLD: {i: 1, v: insert} + NOTICE: TG_level: ROW + NOTICE: TG_name: show_trigger_data_view_trig + NOTICE: TG_op: UPDATE + NOTICE: TG_relatts: {{} i v} + NOTICE: TG_relid: bogus:12345 + NOTICE: TG_table_name: trigger_test_view + NOTICE: TG_table_schema: public + NOTICE: TG_when: {INSTEAD OF} + NOTICE: args: {24 {skidoo view}} + delete from trigger_test_view; + NOTICE: NEW: {} + NOTICE: OLD: {i: 1, v: insert} + NOTICE: TG_level: ROW + NOTICE: TG_name: show_trigger_data_view_trig + NOTICE: TG_op: DELETE + NOTICE: TG_relatts: {{} i v} + NOTICE: TG_relid: bogus:12345 + NOTICE: TG_table_name: trigger_test_view + NOTICE: TG_table_schema: public + NOTICE: TG_when: {INSTEAD OF} + NOTICE: args: {24 {skidoo view}} update trigger_test set v = 'update' where i = 1; NOTICE: NEW: {i: 1, v: update} NOTICE: OLD: {i: 1, v: insert} *** ./src/pl/tcl/expected/pltcl_setup.out.orig 2010-09-04 11:37:59.000000000 +0100 --- ./src/pl/tcl/expected/pltcl_setup.out 2010-09-04 11:43:22.000000000 +0100 *************** *** 51,56 **** --- 51,57 ---- -- dump trigger data CREATE TABLE trigger_test (i int, v text ); + CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test; CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$ if { [info exists TG_relid] } { *************** *** 85,90 **** --- 86,94 ---- CREATE TRIGGER show_trigger_data_trig BEFORE INSERT OR UPDATE OR DELETE ON trigger_test FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + CREATE TRIGGER show_trigger_data_view_trig + INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view + FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view'); -- -- Trigger function on every change to T_pkey1 -- *** ./src/pl/tcl/pltcl.c.orig 2010-07-31 12:26:53.000000000 +0100 --- ./src/pl/tcl/pltcl.c 2010-08-30 23:40:13.000000000 +0100 *************** *** 822,827 **** --- 822,829 ---- Tcl_DStringAppendElement(&tcl_cmd, "BEFORE"); else if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) Tcl_DStringAppendElement(&tcl_cmd, "AFTER"); + else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event)) + Tcl_DStringAppendElement(&tcl_cmd, "INSTEAD OF"); else elog(ERROR, "unrecognized WHEN tg_event: %u", trigdata->tg_event); *** ./src/pl/tcl/sql/pltcl_queries.sql.orig 2010-09-04 11:37:44.000000000 +0100 --- ./src/pl/tcl/sql/pltcl_queries.sql 2010-09-04 22:01:02.000000000 +0100 *************** *** 76,80 **** --- 76,85 ---- -- show dump of trigger data insert into trigger_test values(1,'insert'); + + insert into trigger_test_view values(2,'insert'); + update trigger_test_view set v = 'update' where i=1; + delete from trigger_test_view; + update trigger_test set v = 'update' where i = 1; delete from trigger_test; *** ./src/pl/tcl/sql/pltcl_setup.sql.orig 2010-09-04 11:37:22.000000000 +0100 --- ./src/pl/tcl/sql/pltcl_setup.sql 2010-09-04 11:42:59.000000000 +0100 *************** *** 60,65 **** --- 60,67 ---- CREATE TABLE trigger_test (i int, v text ); + CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test; + CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$ if { [info exists TG_relid] } { *************** *** 96,101 **** --- 98,106 ---- BEFORE INSERT OR UPDATE OR DELETE ON trigger_test FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + CREATE TRIGGER show_trigger_data_view_trig + INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view + FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view'); -- -- Trigger function on every change to T_pkey1 *** ./src/test/regress/expected/triggers.out.orig 2010-08-15 15:55:21.000000000 +0100 --- ./src/test/regress/expected/triggers.out 2010-08-30 23:50:32.000000000 +0100 *************** *** 791,793 **** --- 791,1419 ---- DROP TABLE min_updates_test; DROP TABLE min_updates_test_oids; + -- + -- Test triggers on views + -- + CREATE VIEW main_view AS SELECT a, b FROM main_table; + -- Updates should fail without rules or triggers + INSERT INTO main_view VALUES (1,2); + ERROR: cannot insert into a view + HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger. + UPDATE main_view SET b = 20 WHERE a = 50; + ERROR: cannot update a view + HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger. + DELETE FROM main_view WHERE a = 50; + ERROR: cannot delete from a view + HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger. + -- Should fail even when there are no matching rows + DELETE FROM main_view WHERE a = 51; + ERROR: cannot delete from a view + HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger. + -- VIEW trigger function + CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger + LANGUAGE plpgsql AS $$ + declare + argstr text := ''; + begin + for i in 0 .. TG_nargs - 1 loop + if i > 0 then + argstr := argstr || ', '; + end if; + argstr := argstr || TG_argv[i]; + end loop; + + raise notice '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr; + + if TG_WHEN = 'INSTEAD OF' then + if TG_OP = 'INSERT' then + raise NOTICE 'NEW: %', NEW; + INSERT INTO main_table VALUES (NEW.a, NEW.b); + RETURN NEW; + end if; + + if TG_OP = 'UPDATE' then + raise NOTICE 'OLD: %, NEW: %', OLD, NEW; + UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b; + if NOT FOUND then RETURN NULL; end if; + RETURN NEW; + end if; + + if TG_OP = 'DELETE' then + raise NOTICE 'OLD: %', OLD; + DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b; + if NOT FOUND then RETURN NULL; end if; + RETURN OLD; + end if; + end if; + + RETURN NULL; + end; + $$; + -- Before row triggers aren't allowed on views + CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row'); + ERROR: "main_view" is a view + HINT: Define ROW-level BEFORE and AFTER triggers on tables + CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row'); + ERROR: "main_view" is a view + HINT: Define ROW-level BEFORE and AFTER triggers on tables + CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row'); + ERROR: "main_view" is a view + HINT: Define ROW-level BEFORE and AFTER triggers on tables + -- After row triggers aren't allowed on views + CREATE TRIGGER invalid_trig AFTER INSERT ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row'); + ERROR: "main_view" is a view + HINT: Define ROW-level BEFORE and AFTER triggers on tables + CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row'); + ERROR: "main_view" is a view + HINT: Define ROW-level BEFORE and AFTER triggers on tables + CREATE TRIGGER invalid_trig AFTER DELETE ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row'); + ERROR: "main_view" is a view + HINT: Define ROW-level BEFORE and AFTER triggers on tables + -- Truncate triggers aren't allowed on views + CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view + EXECUTE PROCEDURE trigger_func('before_tru_row'); + ERROR: "main_view" is a view + HINT: Define TRUNCATE triggers on tables + CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view + EXECUTE PROCEDURE trigger_func('before_tru_row'); + ERROR: "main_view" is a view + HINT: Define TRUNCATE triggers on tables + -- INSTEAD OF triggers aren't allowed on tables + CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins'); + ERROR: "main_table" is a table + HINT: Define INSTEAD OF triggers on views + CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd'); + ERROR: "main_table" is a table + HINT: Define INSTEAD OF triggers on views + CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del'); + ERROR: "main_table" is a table + HINT: Define INSTEAD OF triggers on views + -- Don't support WHEN clauses with INSTEAD OF triggers + CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view + FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd'); + ERROR: cannot use a WHEN condition with an INSTEAD OF trigger + -- Don't support column-level INSTEAD OF triggers + CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd'); + ERROR: cannot use a column list with an INSTEAD OF trigger + -- Don't support statement-level INSTEAD OF triggers + CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view + EXECUTE PROCEDURE view_trigger('instead_of_upd'); + ERROR: STATEMENT-level INSTEAD OF triggers are not supported + HINT: Use FOR EACH ROW for INSTEAD OF triggers + -- Valid INSTEAD OF triggers + CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins'); + CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd'); + CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del'); + -- Valid BEFORE statement VIEW triggers + CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt'); + CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt'); + CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt'); + -- Valid AFTER statement VIEW triggers + CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt'); + CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt'); + CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt'); + \set QUIET false + -- Insert into view using trigger + INSERT INTO main_view VALUES (20, 30); + NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt) + NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins) + NOTICE: NEW: (20,30) + NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT + CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)" + PL/pgSQL function "view_trigger" line 17 at SQL statement + NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)" + PL/pgSQL function "view_trigger" line 17 at SQL statement + NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt) + INSERT 0 1 + INSERT INTO main_view VALUES (21, 31) RETURNING a, b; + NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt) + NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins) + NOTICE: NEW: (21,31) + NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT + CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)" + PL/pgSQL function "view_trigger" line 17 at SQL statement + NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)" + PL/pgSQL function "view_trigger" line 17 at SQL statement + NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt) + a | b + ----+---- + 21 | 31 + (1 row) + + INSERT 0 1 + -- Table trigger will prevent updates + UPDATE main_view SET b = 31 WHERE a = 20; + NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt) + NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd) + NOTICE: OLD: (20,30), NEW: (20,31) + NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt) + UPDATE 0 + UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b; + NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt) + NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd) + NOTICE: OLD: (21,31), NEW: (21,32) + NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt) + a | b + ---+--- + (0 rows) + + UPDATE 0 + -- Remove table trigger to allow updates + DROP TRIGGER before_upd_a_row_trig ON main_table; + DROP TRIGGER + UPDATE main_view SET b = 31 WHERE a = 20; + NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt) + NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd) + NOTICE: OLD: (20,30), NEW: (20,31) + NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt) + UPDATE 1 + UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b; + NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt) + NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd) + NOTICE: OLD: (21,31), NEW: (21,32) + NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT + CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b" + PL/pgSQL function "view_trigger" line 23 at SQL statement + NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt) + a | b + ----+---- + 21 | 32 + (1 row) + + UPDATE 1 + -- Before and after stmt triggers should fire even when no rows are affected + UPDATE main_view SET b = 0 WHERE false; + NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt) + NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt) + UPDATE 0 + -- Delete from view using trigger + DELETE FROM main_view WHERE a IN (20,21); + NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt) + NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del) + NOTICE: OLD: (21,10) + NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del) + NOTICE: OLD: (20,31) + NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del) + NOTICE: OLD: (21,32) + NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt) + DELETE 3 + DELETE FROM main_view WHERE a = 31 RETURNING a, b; + NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt) + NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del) + NOTICE: OLD: (31,10) + NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt) + a | b + ----+---- + 31 | 10 + (1 row) + + DELETE 1 + \set QUIET true + -- Describe view should list triggers + \d main_view + View "public.main_view" + Column | Type | Modifiers + --------+---------+----------- + a | integer | + b | integer | + Triggers: + after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt') + after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt') + after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt') + before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt') + before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt') + before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt') + instead_of_delete_trig INSTEAD OF DELETE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del') + instead_of_insert_trig INSTEAD OF INSERT ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins') + instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd') + + -- Test dropping view triggers + DROP TRIGGER instead_of_insert_trig ON main_view; + DROP TRIGGER instead_of_delete_trig ON main_view; + \d+ main_view + View "public.main_view" + Column | Type | Modifiers | Storage | Description + --------+---------+-----------+---------+------------- + a | integer | | plain | + b | integer | | plain | + View definition: + SELECT main_table.a, main_table.b + FROM main_table; + Triggers: + after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt') + after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt') + after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt') + before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt') + before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt') + before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt') + instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd') + + DROP VIEW main_view; + -- + -- Test triggers on a join view + -- + CREATE TABLE country_table ( + country_id serial primary key, + country_name text unique not null, + continent text not null + ); + NOTICE: CREATE TABLE will create implicit sequence "country_table_country_id_seq" for serial column "country_table.country_id" + NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "country_table_pkey" for table "country_table" + NOTICE: CREATE TABLE / UNIQUE will create implicit index "country_table_country_name_key" for table "country_table" + INSERT INTO country_table (country_name, continent) + VALUES ('Japan', 'Asia'), + ('UK', 'Europe'), + ('USA', 'North America') + RETURNING *; + country_id | country_name | continent + ------------+--------------+--------------- + 1 | Japan | Asia + 2 | UK | Europe + 3 | USA | North America + (3 rows) + + CREATE TABLE city_table ( + city_id serial primary key, + city_name text not null, + population bigint, + country_id int references country_table + ); + NOTICE: CREATE TABLE will create implicit sequence "city_table_city_id_seq" for serial column "city_table.city_id" + NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "city_table_pkey" for table "city_table" + CREATE VIEW city_view AS + SELECT city_id, city_name, population, country_name, continent + FROM city_table ci + LEFT JOIN country_table co ON co.country_id = ci.country_id; + CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$ + declare + ctry_id int; + begin + if NEW.country_name IS NOT NULL then + SELECT country_id, continent INTO ctry_id, NEW.continent + FROM country_table WHERE country_name = NEW.country_name; + if NOT FOUND then + raise exception 'No such country: "%"', NEW.country_name; + end if; + else + NEW.continent := NULL; + end if; + + if NEW.city_id IS NOT NULL then + INSERT INTO city_table + VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id); + else + INSERT INTO city_table(city_name, population, country_id) + VALUES(NEW.city_name, NEW.population, ctry_id) + RETURNING city_id INTO NEW.city_id; + end if; + + RETURN NEW; + end; + $$; + CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view + FOR EACH ROW EXECUTE PROCEDURE city_insert(); + CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$ + begin + DELETE FROM city_table WHERE city_id = OLD.city_id; + if NOT FOUND then RETURN NULL; end if; + RETURN OLD; + end; + $$; + CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view + FOR EACH ROW EXECUTE PROCEDURE city_delete(); + CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$ + declare + ctry_id int; + begin + if NEW.country_name IS DISTINCT FROM OLD.country_name then + SELECT country_id, continent INTO ctry_id, NEW.continent + FROM country_table WHERE country_name = NEW.country_name; + if NOT FOUND then + raise exception 'No such country: "%"', NEW.country_name; + end if; + + UPDATE city_table SET city_name = NEW.city_name, + population = NEW.population, + country_id = ctry_id + WHERE city_id = OLD.city_id; + else + UPDATE city_table SET city_name = NEW.city_name, + population = NEW.population + WHERE city_id = OLD.city_id; + NEW.continent := OLD.continent; + end if; + + if NOT FOUND then RETURN NULL; end if; + RETURN NEW; + end; + $$; + CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view + FOR EACH ROW EXECUTE PROCEDURE city_update(); + -- INSERT .. RETURNING + \set QUIET false + INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+----------- + 1 | Tokyo | | | + (1 row) + + INSERT 0 1 + INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+----------- + 2 | London | 7556900 | | + (1 row) + + INSERT 0 1 + INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *; + city_id | city_name | population | country_name | continent + ---------+---------------+------------+--------------+--------------- + 3 | Washington DC | | USA | North America + (1 row) + + INSERT 0 1 + INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+----------- + 123456 | New York | | | + (1 row) + + INSERT 0 1 + INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *; + city_id | city_name | population | country_name | continent + ---------+------------+------------+--------------+----------- + 234567 | Birmingham | 1016800 | UK | Europe + (1 row) + + INSERT 0 1 + -- UPDATE .. RETURNING + UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error + ERROR: No such country: "Japon" + UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match + UPDATE 0 + UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+----------- + 1 | Tokyo | | Japan | Asia + (1 row) + + UPDATE 1 + UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+----------- + 1 | Tokyo | 13010279 | Japan | Asia + (1 row) + + UPDATE 1 + UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+----------- + 123456 | New York | | UK | Europe + (1 row) + + UPDATE 1 + UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+--------------- + 123456 | New York | 8391881 | USA | North America + (1 row) + + UPDATE 1 + UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *; + city_id | city_name | population | country_name | continent + ---------+------------+------------+--------------+----------- + 234567 | Birmingham | 1016800 | UK | Europe + (1 row) + + UPDATE 1 + UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2 + WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *; + city_id | city_name | population | country_name | continent | city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+-----------+---------+------------+------------+--------------+----------- + 2 | London | 7556900 | UK | Europe | 234567 | Birmingham | 1016800 | UK | Europe + (1 row) + + UPDATE 1 + -- DELETE .. RETURNING + DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *; + city_id | city_name | population | country_name | continent + ---------+------------+------------+--------------+----------- + 234567 | Birmingham | 1016800 | UK | Europe + (1 row) + + DELETE 1 + \set QUIET true + -- read-only view with WHERE clause + CREATE VIEW european_city_view AS + SELECT * FROM city_view WHERE continent = 'Europe'; + SELECT count(*) FROM european_city_view; + count + ------- + 1 + (1 row) + + CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql + AS 'begin RETURN NULL; end'; + CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE + ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn(); + \set QUIET false + INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z'); + INSERT 0 0 + UPDATE european_city_view SET population = 10000; + UPDATE 0 + DELETE FROM european_city_view; + DELETE 0 + \set QUIET true + -- rules bypassing no-op triggers + CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view + DO INSTEAD INSERT INTO city_view + VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent) + RETURNING *; + CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view + DO INSTEAD UPDATE city_view SET + city_name = NEW.city_name, + population = NEW.population, + country_name = NEW.country_name + WHERE city_id = OLD.city_id + RETURNING NEW.*; + CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view + DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *; + -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are + \set QUIET false + INSERT INTO european_city_view(city_name, country_name) + VALUES ('Cambridge', 'USA') RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+--------------- + 4 | Cambridge | | USA | North America + (1 row) + + INSERT 0 1 + UPDATE european_city_view SET country_name = 'UK' + WHERE city_name = 'Cambridge'; + UPDATE 0 + DELETE FROM european_city_view WHERE city_name = 'Cambridge'; + DELETE 0 + -- UPDATE and DELETE via rule and trigger + UPDATE city_view SET country_name = 'UK' + WHERE city_name = 'Cambridge' RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+----------- + 4 | Cambridge | | UK | Europe + (1 row) + + UPDATE 1 + UPDATE european_city_view SET population = 122800 + WHERE city_name = 'Cambridge' RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+----------- + 4 | Cambridge | 122800 | UK | Europe + (1 row) + + UPDATE 1 + DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *; + city_id | city_name | population | country_name | continent + ---------+-----------+------------+--------------+----------- + 4 | Cambridge | 122800 | UK | Europe + (1 row) + + DELETE 1 + -- join UPDATE test + UPDATE city_view v SET population = 599657 + FROM city_table ci, country_table co + WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA' + AND v.city_id = ci.city_id AND v.country_name = co.country_name + RETURNING co.country_id, v.country_name, + v.city_id, v.city_name, v.population; + country_id | country_name | city_id | city_name | population + ------------+--------------+---------+---------------+------------ + 3 | USA | 3 | Washington DC | 599657 + (1 row) + + UPDATE 1 + \set QUIET true + SELECT * FROM city_view; + city_id | city_name | population | country_name | continent + ---------+---------------+------------+--------------+--------------- + 1 | Tokyo | 13010279 | Japan | Asia + 123456 | New York | 8391881 | USA | North America + 2 | London | 7556900 | UK | Europe + 3 | Washington DC | 599657 | USA | North America + (4 rows) + + DROP TABLE city_table CASCADE; + NOTICE: drop cascades to 2 other objects + DETAIL: drop cascades to view city_view + drop cascades to view european_city_view + DROP TABLE country_table; *** ./src/test/regress/regress.c.orig 2010-07-31 12:28:32.000000000 +0100 --- ./src/test/regress/regress.c 2010-08-15 12:52:20.000000000 +0100 *************** *** 489,495 **** elog(ERROR, "ttdummy: not fired by trigger manager"); if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) elog(ERROR, "ttdummy: cannot process STATEMENT events"); ! if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) elog(ERROR, "ttdummy: must be fired before event"); if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) elog(ERROR, "ttdummy: cannot process INSERT event"); --- 489,495 ---- elog(ERROR, "ttdummy: not fired by trigger manager"); if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) elog(ERROR, "ttdummy: cannot process STATEMENT events"); ! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event)) elog(ERROR, "ttdummy: must be fired before event"); if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) elog(ERROR, "ttdummy: cannot process INSERT event"); *** ./src/test/regress/sql/triggers.sql.orig 2010-08-19 09:07:19.000000000 +0100 --- ./src/test/regress/sql/triggers.sql 2010-08-30 23:51:39.000000000 +0100 *************** *** 578,580 **** --- 578,937 ---- DROP TABLE min_updates_test; DROP TABLE min_updates_test_oids; + + -- + -- Test triggers on views + -- + + CREATE VIEW main_view AS SELECT a, b FROM main_table; + + -- Updates should fail without rules or triggers + INSERT INTO main_view VALUES (1,2); + UPDATE main_view SET b = 20 WHERE a = 50; + DELETE FROM main_view WHERE a = 50; + -- Should fail even when there are no matching rows + DELETE FROM main_view WHERE a = 51; + + -- VIEW trigger function + CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger + LANGUAGE plpgsql AS $$ + declare + argstr text := ''; + begin + for i in 0 .. TG_nargs - 1 loop + if i > 0 then + argstr := argstr || ', '; + end if; + argstr := argstr || TG_argv[i]; + end loop; + + raise notice '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr; + + if TG_WHEN = 'INSTEAD OF' then + if TG_OP = 'INSERT' then + raise NOTICE 'NEW: %', NEW; + INSERT INTO main_table VALUES (NEW.a, NEW.b); + RETURN NEW; + end if; + + if TG_OP = 'UPDATE' then + raise NOTICE 'OLD: %, NEW: %', OLD, NEW; + UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b; + if NOT FOUND then RETURN NULL; end if; + RETURN NEW; + end if; + + if TG_OP = 'DELETE' then + raise NOTICE 'OLD: %', OLD; + DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b; + if NOT FOUND then RETURN NULL; end if; + RETURN OLD; + end if; + end if; + + RETURN NULL; + end; + $$; + + -- Before row triggers aren't allowed on views + CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row'); + + CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row'); + + CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row'); + + -- After row triggers aren't allowed on views + CREATE TRIGGER invalid_trig AFTER INSERT ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row'); + + CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row'); + + CREATE TRIGGER invalid_trig AFTER DELETE ON main_view + FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row'); + + -- Truncate triggers aren't allowed on views + CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view + EXECUTE PROCEDURE trigger_func('before_tru_row'); + + CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view + EXECUTE PROCEDURE trigger_func('before_tru_row'); + + -- INSTEAD OF triggers aren't allowed on tables + CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins'); + + CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd'); + + CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del'); + + -- Don't support WHEN clauses with INSTEAD OF triggers + CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view + FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd'); + + -- Don't support column-level INSTEAD OF triggers + CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd'); + + -- Don't support statement-level INSTEAD OF triggers + CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view + EXECUTE PROCEDURE view_trigger('instead_of_upd'); + + -- Valid INSTEAD OF triggers + CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins'); + + CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd'); + + CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view + FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del'); + + -- Valid BEFORE statement VIEW triggers + CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt'); + + CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt'); + + CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt'); + + -- Valid AFTER statement VIEW triggers + CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt'); + + CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt'); + + CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view + FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt'); + + \set QUIET false + + -- Insert into view using trigger + INSERT INTO main_view VALUES (20, 30); + INSERT INTO main_view VALUES (21, 31) RETURNING a, b; + + -- Table trigger will prevent updates + UPDATE main_view SET b = 31 WHERE a = 20; + UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b; + + -- Remove table trigger to allow updates + DROP TRIGGER before_upd_a_row_trig ON main_table; + UPDATE main_view SET b = 31 WHERE a = 20; + UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b; + + -- Before and after stmt triggers should fire even when no rows are affected + UPDATE main_view SET b = 0 WHERE false; + + -- Delete from view using trigger + DELETE FROM main_view WHERE a IN (20,21); + DELETE FROM main_view WHERE a = 31 RETURNING a, b; + + \set QUIET true + + -- Describe view should list triggers + \d main_view + + -- Test dropping view triggers + DROP TRIGGER instead_of_insert_trig ON main_view; + DROP TRIGGER instead_of_delete_trig ON main_view; + \d+ main_view + DROP VIEW main_view; + + -- + -- Test triggers on a join view + -- + CREATE TABLE country_table ( + country_id serial primary key, + country_name text unique not null, + continent text not null + ); + + INSERT INTO country_table (country_name, continent) + VALUES ('Japan', 'Asia'), + ('UK', 'Europe'), + ('USA', 'North America') + RETURNING *; + + CREATE TABLE city_table ( + city_id serial primary key, + city_name text not null, + population bigint, + country_id int references country_table + ); + + CREATE VIEW city_view AS + SELECT city_id, city_name, population, country_name, continent + FROM city_table ci + LEFT JOIN country_table co ON co.country_id = ci.country_id; + + CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$ + declare + ctry_id int; + begin + if NEW.country_name IS NOT NULL then + SELECT country_id, continent INTO ctry_id, NEW.continent + FROM country_table WHERE country_name = NEW.country_name; + if NOT FOUND then + raise exception 'No such country: "%"', NEW.country_name; + end if; + else + NEW.continent := NULL; + end if; + + if NEW.city_id IS NOT NULL then + INSERT INTO city_table + VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id); + else + INSERT INTO city_table(city_name, population, country_id) + VALUES(NEW.city_name, NEW.population, ctry_id) + RETURNING city_id INTO NEW.city_id; + end if; + + RETURN NEW; + end; + $$; + + CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view + FOR EACH ROW EXECUTE PROCEDURE city_insert(); + + CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$ + begin + DELETE FROM city_table WHERE city_id = OLD.city_id; + if NOT FOUND then RETURN NULL; end if; + RETURN OLD; + end; + $$; + + CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view + FOR EACH ROW EXECUTE PROCEDURE city_delete(); + + CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$ + declare + ctry_id int; + begin + if NEW.country_name IS DISTINCT FROM OLD.country_name then + SELECT country_id, continent INTO ctry_id, NEW.continent + FROM country_table WHERE country_name = NEW.country_name; + if NOT FOUND then + raise exception 'No such country: "%"', NEW.country_name; + end if; + + UPDATE city_table SET city_name = NEW.city_name, + population = NEW.population, + country_id = ctry_id + WHERE city_id = OLD.city_id; + else + UPDATE city_table SET city_name = NEW.city_name, + population = NEW.population + WHERE city_id = OLD.city_id; + NEW.continent := OLD.continent; + end if; + + if NOT FOUND then RETURN NULL; end if; + RETURN NEW; + end; + $$; + + CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view + FOR EACH ROW EXECUTE PROCEDURE city_update(); + + -- INSERT .. RETURNING + \set QUIET false + + INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *; + INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *; + INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *; + INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *; + INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *; + + -- UPDATE .. RETURNING + UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error + UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match + UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK + + UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *; + UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *; + UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *; + UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *; + UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2 + WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *; + + -- DELETE .. RETURNING + DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *; + + \set QUIET true + + -- read-only view with WHERE clause + CREATE VIEW european_city_view AS + SELECT * FROM city_view WHERE continent = 'Europe'; + SELECT count(*) FROM european_city_view; + + CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql + AS 'begin RETURN NULL; end'; + + CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE + ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn(); + + \set QUIET false + + INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z'); + UPDATE european_city_view SET population = 10000; + DELETE FROM european_city_view; + + \set QUIET true + + -- rules bypassing no-op triggers + CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view + DO INSTEAD INSERT INTO city_view + VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent) + RETURNING *; + + CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view + DO INSTEAD UPDATE city_view SET + city_name = NEW.city_name, + population = NEW.population, + country_name = NEW.country_name + WHERE city_id = OLD.city_id + RETURNING NEW.*; + + CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view + DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *; + + -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are + \set QUIET false + + INSERT INTO european_city_view(city_name, country_name) + VALUES ('Cambridge', 'USA') RETURNING *; + UPDATE european_city_view SET country_name = 'UK' + WHERE city_name = 'Cambridge'; + DELETE FROM european_city_view WHERE city_name = 'Cambridge'; + + -- UPDATE and DELETE via rule and trigger + UPDATE city_view SET country_name = 'UK' + WHERE city_name = 'Cambridge' RETURNING *; + UPDATE european_city_view SET population = 122800 + WHERE city_name = 'Cambridge' RETURNING *; + DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *; + + -- join UPDATE test + UPDATE city_view v SET population = 599657 + FROM city_table ci, country_table co + WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA' + AND v.city_id = ci.city_id AND v.country_name = co.country_name + RETURNING co.country_id, v.country_name, + v.city_id, v.city_name, v.population; + + \set QUIET true + + SELECT * FROM city_view; + + DROP TABLE city_table CASCADE; + DROP TABLE country_table;