diff --git a/contrib/spi/autoinc.c b/contrib/spi/autoinc.c
index 4552fc3..a3373e6 100644
*** a/contrib/spi/autoinc.c
--- b/contrib/spi/autoinc.c
*************** autoinc(PG_FUNCTION_ARGS)
*** 38,44 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
--- 38,44 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "must be fired before event");
diff --git a/contrib/spi/insert_username.c b/contrib/spi/insert_username.c
index da6764f..3af8c03 100644
*** a/contrib/spi/insert_username.c
--- b/contrib/spi/insert_username.c
*************** insert_username(PG_FUNCTION_ARGS)
*** 41,47 ****
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
--- 41,47 ----
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "insert_username: must be fired before event");
diff --git a/contrib/spi/moddatetime.c b/contrib/spi/moddatetime.c
index e90c0b5..3a2fdf7 100644
*** a/contrib/spi/moddatetime.c
--- b/contrib/spi/moddatetime.c
*************** moddatetime(PG_FUNCTION_ARGS)
*** 47,53 ****
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
--- 47,53 ----
/* internal error */
elog(ERROR, "moddatetime: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
/* internal error */
elog(ERROR, "moddatetime: must be fired before event");
diff --git a/contrib/spi/timetravel.c b/contrib/spi/timetravel.c
index 8bae313..e215670 100644
*** a/contrib/spi/timetravel.c
--- b/contrib/spi/timetravel.c
*************** timetravel(PG_FUNCTION_ARGS)
*** 122,128 ****
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
--- 122,128 ----
elog(ERROR, "timetravel: cannot process STATEMENT events");
/* Should be called BEFORE */
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "timetravel: must be fired before event");
/* INSERT ? */
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 8e4081c..b7da10b 100644
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 4795,4801 ****
! The catalog pg_trigger stores triggers on tables.
See
for more information.
--- 4795,4801 ----
! The catalog pg_trigger stores triggers on tables and views.
See
for more information.
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 509efea..7c039a1 100644
*** a/doc/src/sgml/information_schema.sgml
--- b/doc/src/sgml/information_schema.sgml
*************** ORDER BY c.ordinal_position;
*** 4885,4892 ****
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.
*************** ORDER BY c.ordinal_position;
*** 4990,4997 ****
condition_timing
character_data
! Time at which the trigger fires (BEFORE or
! AFTER)
--- 4990,4997 ----
condition_timing
character_data
! Time at which the trigger fires (BEFORE,
! AFTER or INSTEAD OF)
*************** ORDER BY c.ordinal_position;
*** 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
!
diff --git a/doc/src/sgml/plperl.sgml b/doc/src/sgml/plperl.sgml
index 864b53d..4dee321 100644
*** a/doc/src/sgml/plperl.sgml
--- b/doc/src/sgml/plperl.sgml
*************** $$ LANGUAGE plperl;
*** 980,986 ****
$_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
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index d3bf847..4bbec80 100644
*** a/doc/src/sgml/plpgsql.sgml
--- b/doc/src/sgml/plpgsql.sgml
*************** RAISE unique_violation USING MESSAGE = '
*** 3112,3120 ****
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.
*************** RAISE unique_violation USING MESSAGE = '
*** 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
*************** AFTER INSERT OR UPDATE OR DELETE ON emp
*** 3355,3360 ****
--- 3370,3454 ----
+ 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 PL/pgSQL 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
original table for certain queries — often with vastly reduced run
diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml
index c544563..1774b2b 100644
*** a/doc/src/sgml/plpython.sgml
--- b/doc/src/sgml/plpython.sgml
*************** $$ LANGUAGE plpythonu;
*** 762,768 ****
contains one of BEFORE>, AFTER>,
! or UNKNOWN>.
--- 762,768 ----
contains one of BEFORE>, AFTER>,
! INSTEAD OF> or UNKNOWN>.
*************** $$ LANGUAGE plpythonu;
*** 838,849 ****
! 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.
--- 838,851 ----
! 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.
diff --git a/doc/src/sgml/pltcl.sgml b/doc/src/sgml/pltcl.sgml
index eb29a8f..833ee1f 100644
*** a/doc/src/sgml/pltcl.sgml
--- b/doc/src/sgml/pltcl.sgml
*************** SELECT 'doesn''t' AS ret
*** 567,574 ****
$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.
*************** SELECT 'doesn''t' AS ret
*** 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.
diff --git a/doc/src/sgml/ref/create_rule.sgml b/doc/src/sgml/ref/create_rule.sgml
index 5d2182c..d70d1f4 100644
*** a/doc/src/sgml/ref/create_rule.sgml
--- b/doc/src/sgml/ref/create_rule.sgml
*************** CREATE [ OR REPLACE ] RULE 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 ).
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index 1934113..9b26d63 100644
*** a/doc/src/sgml/ref/create_trigger.sgml
--- b/doc/src/sgml/ref/create_trigger.sgml
*************** PostgreSQL documentation
*** 21,27 ****
! 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 )
*************** CREATE TRIGGER 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
*************** CREATE TRIGGER
+
+
+
+ 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.
*************** CREATE TRIGGER 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.
*************** UPDATE OF column_name1
+ INSTEAD OF> triggers do not support WHEN>
+ conditions.
+
+
+
Currently, WHEN expressions cannot contain
subqueries.
*************** CREATE TRIGGER log_update
*** 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();
+
*************** CREATE TRIGGER log_update
*** 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.
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index c42ff29..ca91ac0 100644
*** a/doc/src/sgml/rules.sgml
--- b/doc/src/sgml/rules.sgml
***************
*** 168,176 ****
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.
*************** SELECT shoe_ready.shoename, shoe_ready.s
*** 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.
*************** SELECT t1.a, t2.b FROM t1, t2 WHERE t1.a
*** 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
*************** SELECT t1.a, t2.b, t1.ctid FROM t1, t2 W
*** 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
! ).
*************** SELECT t1.a, t2.b, t1.ctid FROM t1, t2 W
*** 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.
*************** GRANT SELECT ON phone_number TO secretar
*** 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.
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 3565bff..58296fd 100644
*** a/doc/src/sgml/trigger.sgml
--- b/doc/src/sgml/trigger.sgml
***************
*** 33,39 ****
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,120 ----
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 according to whether they fire
! before>, after> or
! instead of> the operation. These are referred to
! as BEFORE> triggers, AFTER> triggers and
! INSTEAD OF> triggers respectively.
! 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 (after any statement-level BEFORE> triggers,
! and before any statement-level AFTER> triggers on the view).
***************
*** 117,124 ****
! A row-level before trigger that does not intend to cause either of
! these behaviors must be careful to return as its result the same
row that was passed in (that is, the NEW row
for INSERT and UPDATE
triggers, the OLD row for
--- 146,153 ----
! A row-level BEFORE> trigger that does not intend to cause
! either of these behaviors must be careful to return as its result the same
row that was passed in (that is, the NEW row
for INSERT and UPDATE
triggers, the OLD row for
***************
*** 126,131 ****
--- 155,176 ----
+ 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,143 ****
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.
--- 178,189 ----
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 (for that row).
***************
*** 146,176 ****
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.) In a before trigger, the WHEN>
condition is evaluated just before the function is or would be executed,
so using WHEN> is not materially different from testing the
same condition at the beginning of the trigger function. However, in
! an after trigger, the WHEN> condition is evaluated just after
! the row update occurs, and it determines whether an event is queued to
! fire the trigger at the end of statement. So when an after trigger's
WHEN> condition does not return true, it is not necessary
to queue an event nor to re-fetch the row at end of statement. This
can result in significant speedups in statements that modify many
rows, if the trigger only needs to be fired for a few of the rows.
! Typically, row before triggers are used for checking or
modifying the data that will be inserted or updated. For example,
! a before trigger might be used to insert the current time into a
timestamp column, or to check that two elements of the row are
! consistent. Row after triggers are most sensibly
used to propagate the updates to other tables, or make consistency
checks against other tables. The reason for this division of labor is
! that an after trigger can be certain it is seeing the final value of the
! row, while a before trigger cannot; there might be other before triggers
! firing after it. If you have no specific reason to make a trigger before
! or after, the before case is more efficient, since the information about
the operation doesn't have to be saved until end of statement.
--- 192,228 ----
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.) In a BEFORE> trigger, the
! WHEN>
condition is evaluated just before the function is or would be executed,
so using WHEN> is not materially different from testing the
same condition at the beginning of the trigger function. However, in
! an AFTER> trigger, the WHEN> condition is evaluated
! just after the row update occurs, and it determines whether an event is
! queued to fire the trigger at the end of statement. So when an
! AFTER> trigger's
WHEN> condition does not return true, it is not necessary
to queue an event nor to re-fetch the row at end of statement. This
can result in significant speedups in statements that modify many
rows, if the trigger only needs to be fired for a few of the rows.
+ INSTEAD OF> triggers on views do not support
+ WHEN> conditions.
! Typically, row-level BEFORE> triggers are used for checking or
modifying the data that will be inserted or updated. For example,
! a BEFORE> trigger might be used to insert the current time into a
timestamp column, or to check that two elements of the row are
! consistent. Row-level AFTER> triggers are most sensibly
used to propagate the updates to other tables, or make consistency
checks against other tables. The reason for this division of labor is
! that an AFTER> trigger can be certain it is seeing the final
! value of the row, while a BEFORE> trigger cannot; there might
! be other BEFORE> triggers firing after it. If you have no
! specific reason to make a trigger BEFORE> or
! AFTER>, the BEFORE> case is more efficient, since
! the information about
the operation doesn't have to be saved until end of statement.
***************
*** 237,243 ****
Statement-level triggers follow simple visibility rules: none of
the changes made by a statement are visible to statement-level
triggers that are invoked before the statement, whereas all
! modifications are visible to statement-level after triggers.
--- 289,296 ----
Statement-level triggers follow simple visibility rules: none of
the changes made by a statement are visible to statement-level
triggers that are invoked before the statement, whereas all
! modifications are visible to statement-level AFTER>
! triggers.
***************
*** 245,258 ****
The data change (insertion, update, or deletion) causing the
trigger to fire is naturally not visible
! to SQL commands executed in a row-level before trigger, because
! it hasn't happened yet.
! However, SQL commands executed in a row-level before
trigger will see the effects of data
changes for rows previously processed in the same outer
command. This requires caution, since the ordering of these
--- 298,311 ----
The data change (insertion, update, or deletion) causing the
trigger to fire is naturally not visible
! to SQL commands executed in a row-level BEFORE> trigger,
! because it hasn't happened yet.
! However, SQL commands executed in a row-level BEFORE>
trigger will see the effects of data
changes for rows previously processed in the same outer
command. This requires caution, since the ordering of these
***************
*** 263,269 ****
! When a row-level after trigger is fired, all data changes made
by the outer command are already complete, and are visible to
the invoked trigger function.
--- 316,323 ----
! When a row-level AFTER> trigger is fired, all data
! changes made
by the outer command are already complete, and are visible to
the invoked trigger function.
*************** typedef struct TriggerData
*** 378,383 ****
--- 432,446 ----
+ TRIGGER_FIRED_INSTEAD(tg_event)
+
+
+ Returns true if the trigger fired instead of the operation.
+
+
+
+
+
TRIGGER_FIRED_AFTER(tg_event)
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 2b92e46..001f459 100644
*** a/src/backend/catalog/index.c
--- b/src/backend/catalog/index.c
*************** index_create(Oid heapRelationId,
*** 825,830 ****
--- 825,831 ----
trigger->funcname = SystemFuncName("unique_key_recheck");
trigger->args = NIL;
trigger->before = false;
+ trigger->instead = false;
trigger->row = true;
trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE;
trigger->columns = NIL;
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index bd81222..1641048 100644
*** a/src/backend/catalog/information_schema.sql
--- b/src/backend/catalog/information_schema.sql
*************** CREATE VIEW triggers AS
*** 1937,1943 ****
CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END
AS character_data) AS action_orientation,
CAST(
! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' ELSE 'AFTER' END
AS character_data) AS condition_timing,
CAST(null AS sql_identifier) AS condition_reference_old_table,
CAST(null AS sql_identifier) AS condition_reference_new_table,
--- 1937,1943 ----
CASE WHEN t.tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END
AS character_data) AS action_orientation,
CAST(
! CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' WHEN t.tgtype & 64 = 64 THEN 'INSTEAD OF' ELSE 'AFTER' END
AS character_data) AS condition_timing,
CAST(null AS sql_identifier) AS condition_reference_old_table,
CAST(null AS sql_identifier) AS condition_reference_new_table,
*************** CREATE VIEW views AS
*** 2233,2241 ****
THEN 'YES' ELSE 'NO' END
AS yes_or_no) AS is_insertable_into,
! CAST('NO' AS yes_or_no) AS is_trigger_updatable,
! CAST('NO' AS yes_or_no) AS is_trigger_deletable,
! CAST('NO' AS yes_or_no) AS is_trigger_insertable_into
FROM pg_namespace nc, pg_class c
--- 2233,2252 ----
THEN 'YES' ELSE 'NO' END
AS yes_or_no) AS is_insertable_into,
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 80 = 80)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_updatable,
!
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 72 = 72)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_deletable,
!
! CAST(
! CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 68 = 68)
! THEN 'YES' ELSE 'NO' END
! AS yes_or_no) AS is_trigger_insertable_into
FROM pg_namespace nc, pg_class c
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index bfcfdfc..b733f4c 100644
*** a/src/backend/catalog/sql_features.txt
--- b/src/backend/catalog/sql_features.txt
*************** T211 Basic trigger capability 06 Support
*** 426,432 ****
T211 Basic trigger capability 07 TRIGGER privilege YES
T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted
T212 Enhanced trigger capability YES
! T213 INSTEAD OF triggers NO
T231 Sensitive cursors YES
T241 START TRANSACTION statement YES
T251 SET TRANSACTION statement: LOCAL option NO
--- 426,432 ----
T211 Basic trigger capability 07 TRIGGER privilege YES
T211 Basic trigger capability 08 Multiple triggers for the same event are executed in the order in which they were created in the catalog NO intentionally omitted
T212 Enhanced trigger capability YES
! T213 INSTEAD OF triggers YES
T231 Sensitive cursors YES
T241 START TRANSACTION statement YES
T251 SET TRANSACTION statement: LOCAL option NO
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a5d7af6..9bcdbf0 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** CreateFKCheckTrigger(RangeVar *myRel, Co
*** 5691,5696 ****
--- 5691,5697 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = myRel;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
/* Either ON INSERT or ON UPDATE */
*************** createForeignKeyTriggers(Relation rel, C
*** 5754,5759 ****
--- 5755,5761 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_DELETE;
fk_trigger->columns = NIL;
*************** createForeignKeyTriggers(Relation rel, C
*** 5807,5812 ****
--- 5809,5815 ----
fk_trigger->trigname = "RI_ConstraintTrigger";
fk_trigger->relation = fkconstraint->pktable;
fk_trigger->before = false;
+ fk_trigger->instead = false;
fk_trigger->row = true;
fk_trigger->events = TRIGGER_TYPE_UPDATE;
fk_trigger->columns = NIL;
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index f0b32dd..2c960e2 100644
*** a/src/backend/commands/trigger.c
--- b/src/backend/commands/trigger.c
*************** CreateTrigger(CreateTrigStmt *stmt, cons
*** 150,159 ****
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 150,198 ----
*/
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind == RELKIND_RELATION)
! {
! /* Forbid INSTEAD OF triggers on tables */
! if (stmt->instead)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a table",
! RelationGetRelationName(rel)),
! errhint("Define INSTEAD OF triggers on views")));
! }
! else if (rel->rd_rel->relkind == RELKIND_VIEW)
! {
! /* Only allow INSTEAD OF or STATEMENT-level triggers on views */
! if (!stmt->instead && stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is a view",
! RelationGetRelationName(rel)),
! errhint("Define ROW-level BEFORE and AFTER triggers on tables")));
!
! /* Disallow STATEMENT-level INSTEAD OF triggers */
! if (stmt->instead && !stmt->row)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("STATEMENT-level INSTEAD OF triggers are not supported"),
! errhint("Use FOR EACH ROW for INSTEAD OF triggers")));
!
! /* Disallow WHEN clauses with INSTEAD OF triggers */
! if (stmt->instead && stmt->whenClause)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a WHEN condition with an INSTEAD OF trigger")));
!
! /* Disallow column selection with INSTEAD OF triggers */
! if (stmt->instead && stmt->columns != NIL)
! ereport(ERROR,
! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! errmsg("cannot use a column list with an INSTEAD OF trigger")));
! }
! else
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
*************** CreateTrigger(CreateTrigStmt *stmt, cons
*** 188,193 ****
--- 227,234 ----
TRIGGER_CLEAR_TYPE(tgtype);
if (stmt->before)
TRIGGER_SETT_BEFORE(tgtype);
+ if (stmt->instead)
+ TRIGGER_SETT_INSTEAD(tgtype);
if (stmt->row)
TRIGGER_SETT_ROW(tgtype);
tgtype |= stmt->events;
*************** CreateTrigger(CreateTrigStmt *stmt, cons
*** 198,203 ****
--- 239,252 ----
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("TRUNCATE FOR EACH ROW triggers are not supported")));
+ /* Disallow TRUNCATE triggers on VIEWs */
+ if (rel->rd_rel->relkind == RELKIND_VIEW && TRIGGER_FOR_TRUNCATE(tgtype))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is a view",
+ RelationGetRelationName(rel)),
+ errhint("Define TRUNCATE triggers on tables")));
+
/*
* Parse the WHEN clause, if any
*/
*************** RemoveTriggerById(Oid trigOid)
*** 1031,1040 ****
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
--- 1080,1090 ----
rel = heap_open(relid, ShareRowExclusiveLock);
! if (rel->rd_rel->relkind != RELKIND_RELATION &&
! rel->rd_rel->relkind != RELKIND_VIEW)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("\"%s\" is not a table or view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
*************** InsertTrigger(TriggerDesc *trigdesc, Tri
*** 1506,1511 ****
--- 1556,1566 ----
n = trigdesc->n_before_row;
t = trigdesc->tg_before_row;
}
+ else if (TRIGGER_FOR_INSTEAD(trigger->tgtype))
+ {
+ n = trigdesc->n_instead_row;
+ t = trigdesc->tg_instead_row;
+ }
else
{
n = trigdesc->n_after_row;
*************** CopyTriggerDesc(TriggerDesc *trigdesc)
*** 1656,1661 ****
--- 1711,1729 ----
else
t[i] = NULL;
}
+ n = newdesc->n_instead_row;
+ t = newdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ {
+ if (n[i] > 0)
+ {
+ tnew = (int *) palloc(n[i] * sizeof(int));
+ memcpy(tnew, t[i], n[i] * sizeof(int));
+ t[i] = tnew;
+ }
+ else
+ t[i] = NULL;
+ }
n = newdesc->n_after_statement;
t = newdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
*************** FreeTriggerDesc(TriggerDesc *trigdesc)
*** 1698,1703 ****
--- 1766,1775 ----
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
pfree(t[i]);
+ t = trigdesc->tg_instead_row;
+ for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
+ if (t[i] != NULL)
+ pfree(t[i]);
t = trigdesc->tg_after_statement;
for (i = 0; i < TRIGGER_NUM_EVENT_CLASSES; i++)
if (t[i] != NULL)
*************** ExecARInsertTriggers(EState *estate, Res
*** 2000,2005 ****
--- 2072,2121 ----
true, NULL, trigtuple, recheckIndexes, NULL);
}
+ HeapTuple
+ ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_INSERT];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_INSERT];
+ TriggerData LocTriggerData;
+ HeapTuple newtuple = trigtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, NULL, trigtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != trigtuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
{
*************** ExecARDeleteTriggers(EState *estate, Res
*** 2134,2139 ****
--- 2250,2297 ----
}
}
+ bool
+ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple trigtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_DELETE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_DELETE];
+ TriggerData LocTriggerData;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, trigtuple, NULL))
+ continue;
+
+ LocTriggerData.tg_trigtuple = trigtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuple = NULL;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple == NULL)
+ return false; /* Delete was suppressed */
+ if (rettuple != trigtuple)
+ heap_freetuple(rettuple);
+ }
+ return true;
+ }
+
void
ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
{
*************** ExecARUpdateTriggers(EState *estate, Res
*** 2281,2286 ****
--- 2439,2488 ----
}
}
+ HeapTuple
+ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ HeapTuple oldtuple, HeapTuple newtuple)
+ {
+ TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
+ int ntrigs = trigdesc->n_instead_row[TRIGGER_EVENT_UPDATE];
+ int *tgindx = trigdesc->tg_instead_row[TRIGGER_EVENT_UPDATE];
+ TriggerData LocTriggerData;
+ HeapTuple intuple = newtuple;
+ HeapTuple rettuple;
+ int i;
+
+ LocTriggerData.type = T_TriggerData;
+ LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
+ TRIGGER_EVENT_ROW |
+ TRIGGER_EVENT_INSTEAD;
+ LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ for (i = 0; i < ntrigs; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[tgindx[i]];
+
+ if (!TriggerEnabled(estate, relinfo, trigger, LocTriggerData.tg_event,
+ NULL, oldtuple, newtuple))
+ continue;
+
+ LocTriggerData.tg_trigtuple = oldtuple;
+ LocTriggerData.tg_newtuple = newtuple;
+ LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_newtuplebuf = InvalidBuffer;
+ LocTriggerData.tg_trigger = trigger;
+ rettuple = ExecCallTriggerFunc(&LocTriggerData,
+ tgindx[i],
+ relinfo->ri_TrigFunctions,
+ relinfo->ri_TrigInstrument,
+ GetPerTupleMemoryContext(estate));
+ if (rettuple != newtuple && newtuple != intuple)
+ heap_freetuple(newtuple);
+ newtuple = rettuple;
+ if (newtuple == NULL)
+ break;
+ }
+ return newtuple;
+ }
+
void
ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
{
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 20af966..3a7b614 100644
*** a/src/backend/executor/execMain.c
--- b/src/backend/executor/execMain.c
*************** InitResultRelInfo(ResultRelInfo *resultR
*** 899,908 ****
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! ereport(ERROR,
! (errcode(ERRCODE_WRONG_OBJECT_TYPE),
! errmsg("cannot change view \"%s\"",
! RelationGetRelationName(resultRelationDesc))));
break;
default:
ereport(ERROR,
--- 899,905 ----
RelationGetRelationName(resultRelationDesc))));
break;
case RELKIND_VIEW:
! /* OK */
break;
default:
ereport(ERROR,
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index a9958eb..e6796b6 100644
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
*************** ExecProcessReturning(ProjectionInfo *pro
*** 149,154 ****
--- 149,200 ----
}
/* ----------------------------------------------------------------
+ * ExecCheckViewTriggers
+ *
+ * If the result relation is a view, check that it has the
+ * appropriate INSTEAD OF triggers to carry out the current
+ * operation.
+ * ----------------------------------------------------------------
+ */
+ static void
+ ExecCheckViewTriggers(ResultRelInfo *resultRelInfo,
+ CmdType operation)
+ {
+ TriggerDesc *trigDesc = resultRelInfo->ri_TrigDesc;
+
+ switch (operation)
+ {
+ case CMD_INSERT:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_INSERT] == 0)
+ ereport(ERROR,
+ (errmsg("cannot insert into a view"),
+ errhint("You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.")));
+ break;
+
+ case CMD_UPDATE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_UPDATE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot update a view"),
+ errhint("You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.")));
+ break;
+
+ case CMD_DELETE:
+ if (!trigDesc ||
+ trigDesc->n_instead_row[TRIGGER_EVENT_DELETE] == 0)
+ ereport(ERROR,
+ (errmsg("cannot delete from a view"),
+ errhint("You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.")));
+ break;
+
+ default:
+ elog(ERROR, "unknown operation");
+ break;
+ }
+ }
+
+ /* ----------------------------------------------------------------
* ExecInsert
*
* For INSERT, we have to insert the tuple into the target relation
*************** ExecInsert(TupleTableSlot *slot,
*** 167,172 ****
--- 213,219 ----
Relation resultRelationDesc;
Oid newId;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* get the heap tuple out of the tuple table slot, making sure we have a
*************** ExecInsert(TupleTableSlot *slot,
*** 179,184 ****
--- 226,232 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
/*
* If the result relation has OIDs, force the tuple's OID to zero so that
*************** ExecInsert(TupleTableSlot *slot,
*** 195,202 ****
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /* BEFORE ROW INSERT Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
--- 243,251 ----
if (resultRelationDesc->rd_rel->relhasoids)
HeapTupleSetOid(tuple, InvalidOid);
! /* BEFORE ROW INSERT Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_INSERT] > 0)
{
HeapTuple newtuple;
*************** ExecInsert(TupleTableSlot *slot,
*** 237,244 ****
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
(estate->es_processed)++;
estate->es_lastoid = newId;
--- 286,329 ----
* Note: heap_insert returns the tid (location) of the new tuple in the
* t_self field.
*/
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual insert
! */
! HeapTuple newtuple;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_INSERT);
! newtuple = ExecIRInsertTriggers(estate, resultRelInfo, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. We assume the tuple was allocated in
! * per-tuple memory context, and therefore will go away by
! * itself. The tuple table slot should not try to clear it.
! */
! TupleTableSlot *newslot = estate->es_trig_tuple_slot;
! TupleDesc tupdesc = RelationGetDescr(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! newId = InvalidOid;
! }
! else
! {
! newId = heap_insert(resultRelationDesc, tuple,
! estate->es_output_cid, 0, NULL);
! }
(estate->es_processed)++;
estate->es_lastoid = newId;
*************** ExecInsert(TupleTableSlot *slot,
*** 251,258 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW INSERT Triggers */
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
--- 336,344 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW INSERT Triggers (tables only) */
! if (!resultRelIsView)
! ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
list_free(recheckIndexes);
*************** ExecInsert(TupleTableSlot *slot,
*** 268,280 ****
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
--- 354,372 ----
* ExecDelete
*
* DELETE is like UPDATE, except that we delete the tuple and no
! * index modifications are needed.
! *
! * When deleting from a table, tupleid identifies the tuple to
! * delete and oldtuple is NULL. When deleting from a view,
! * oldtuple is passed to the INSTEAD OF triggers and identifies
! * what to delete, and tupleid is invalid.
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecDelete(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate)
*************** ExecDelete(ItemPointer tupleid,
*** 284,298 ****
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW DELETE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
--- 376,393 ----
HTSU_Result result;
ItemPointerData update_ctid;
TransactionId update_xmax;
+ bool resultRelIsView;
/*
* get information on the (current) result relation
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /* BEFORE ROW DELETE Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_DELETE] > 0)
{
bool dodelete;
*************** ExecDelete(ItemPointer tupleid,
*** 313,323 ****
* referential integrity updates in transaction-snapshot mode transactions.
*/
ldelete:;
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 408,443 ----
* referential integrity updates in transaction-snapshot mode transactions.
*/
ldelete:;
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual delete
! */
! HeapTupleData tuple;
! bool diddelete;
!
! tuple.t_data = oldtuple;
! tuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(tuple.t_self));
! tuple.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_DELETE);
! diddelete = ExecIRDeleteTriggers(estate, resultRelInfo, &tuple);
!
! if (!diddelete) /* trigger(s) did nothing */
! return NULL;
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_delete(resultRelationDesc, tupleid,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
*************** ldelete:;
*** 367,374 ****
* anyway, since the tuple is still visible to other transactions.
*/
! /* AFTER ROW DELETE Triggers */
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 487,495 ----
* anyway, since the tuple is still visible to other transactions.
*/
! /* AFTER ROW DELETE Triggers (tables only) */
! if (!resultRelIsView)
! ExecARDeleteTriggers(estate, resultRelInfo, tupleid);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
*************** ldelete:;
*** 382,391 ****
HeapTupleData deltuple;
Buffer delbuffer;
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
--- 503,522 ----
HeapTupleData deltuple;
Buffer delbuffer;
! if (resultRelIsView)
! {
! deltuple.t_data = oldtuple;
! deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(deltuple.t_self));
! deltuple.t_tableOid = InvalidOid;
! }
! else
! {
! deltuple.t_self = *tupleid;
! if (!heap_fetch(resultRelationDesc, SnapshotAny,
! &deltuple, &delbuffer, false, NULL))
! elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
! }
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
*************** ldelete:;
*** 395,401 ****
slot, planSlot);
ExecClearTuple(slot);
! ReleaseBuffer(delbuffer);
return rslot;
}
--- 526,533 ----
slot, planSlot);
ExecClearTuple(slot);
! if (!resultRelIsView)
! ReleaseBuffer(delbuffer);
return rslot;
}
*************** ldelete:;
*** 413,423 ****
--- 545,561 ----
* is, we don't want to get stuck in an infinite loop
* which corrupts your database..
*
+ * When updating a table, tupleid identifies the tuple to
+ * update and oldtuple is NULL. When updating a view, oldtuple
+ * is passed to the INSTEAD OF triggers and identifies what to
+ * update, and tupleid is invalid.
+ *
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
static TupleTableSlot *
ExecUpdate(ItemPointer tupleid,
+ HeapTupleHeader oldtuple,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EPQState *epqstate,
*************** ExecUpdate(ItemPointer tupleid,
*** 430,435 ****
--- 568,574 ----
ItemPointerData update_ctid;
TransactionId update_xmax;
List *recheckIndexes = NIL;
+ bool resultRelIsView;
/*
* abort the operation if not running transactions
*************** ExecUpdate(ItemPointer tupleid,
*** 448,456 ****
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
! /* BEFORE ROW UPDATE Triggers */
! if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
--- 587,597 ----
*/
resultRelInfo = estate->es_result_relation_info;
resultRelationDesc = resultRelInfo->ri_RelationDesc;
+ resultRelIsView = resultRelationDesc->rd_rel->relkind == RELKIND_VIEW;
! /* BEFORE ROW UPDATE Triggers (tables only) */
! if (!resultRelIsView &&
! resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->n_before_row[TRIGGER_EVENT_UPDATE] > 0)
{
HeapTuple newtuple;
*************** lreplace:;
*** 501,511 ****
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in transaction-snapshot mode transactions.
*/
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
switch (result)
{
case HeapTupleSelfUpdated:
--- 642,696 ----
* serialize error if not. This is a special-case behavior needed for
* referential integrity updates in transaction-snapshot mode transactions.
*/
! if (resultRelIsView)
! {
! /*
! * INSTEAD OF trigger(s) do the actual update
! */
! HeapTupleData oldtup;
! HeapTuple newtuple;
!
! oldtup.t_data = oldtuple;
! oldtup.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
! ItemPointerSetInvalid(&(oldtup.t_self));
! oldtup.t_tableOid = InvalidOid;
!
! ExecCheckViewTriggers(resultRelInfo, CMD_UPDATE);
! newtuple = ExecIRUpdateTriggers(estate, resultRelInfo,
! &oldtup, tuple);
!
! if (newtuple == NULL) /* trigger(s) did nothing */
! return NULL;
!
! if (newtuple != tuple) /* modified by Trigger(s) */
! {
! /*
! * Put the modified tuple into a slot for convenience of
! * routines below. We assume the tuple was allocated in
! * per-tuple memory context, and therefore will go away by
! * itself. The tuple table slot should not try to clear it.
! */
! TupleTableSlot *newslot = estate->es_trig_tuple_slot;
! TupleDesc tupdesc = RelationGetDescr(resultRelationDesc);
!
! if (newslot->tts_tupleDescriptor != tupdesc)
! ExecSetSlotDescriptor(newslot, tupdesc);
! ExecStoreTuple(newtuple, newslot, InvalidBuffer, false);
! slot = newslot;
! tuple = newtuple;
! }
!
! result = HeapTupleMayBeUpdated;
! }
! else
! {
! result = heap_update(resultRelationDesc, tupleid, tuple,
! &update_ctid, &update_xmax,
! estate->es_output_cid,
! estate->es_crosscheck_snapshot,
! true /* wait for commit */ );
! }
!
switch (result)
{
case HeapTupleSelfUpdated:
*************** lreplace:;
*** 568,576 ****
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW UPDATE Triggers */
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
--- 753,762 ----
recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
estate);
! /* AFTER ROW UPDATE Triggers (tables only) */
! if (!resultRelIsView)
! ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple,
! recheckIndexes);
list_free(recheckIndexes);
*************** ExecModifyTable(ModifyTableState *node)
*** 654,659 ****
--- 840,846 ----
TupleTableSlot *planSlot;
ItemPointer tupleid = NULL;
ItemPointerData tuple_ctid;
+ HeapTupleHeader oldtuple;
/*
* On first call, fire BEFORE STATEMENT triggers before proceeding.
*************** ExecModifyTable(ModifyTableState *node)
*** 713,735 ****
if (junkfilter != NULL)
{
/*
! * extract the 'ctid' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! /* shouldn't ever get a null result... */
! if (isNull)
! elog(ERROR, "ctid is NULL");
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
}
/*
--- 900,936 ----
if (junkfilter != NULL)
{
+ ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
+ Relation resultRel = resultRelInfo->ri_RelationDesc;
+
/*
! * extract the 'ctid' or 'wholerow' junk attribute.
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
Datum datum;
bool isNull;
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "wholerow is NULL");
! oldtuple = DatumGetHeapTupleHeader(datum);
! }
! else
! {
! datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo,
! &isNull);
! if (isNull)
! elog(ERROR, "ctid is NULL");
!
! tupleid = (ItemPointer) DatumGetPointer(datum);
! tuple_ctid = *tupleid; /* be sure we don't free the ctid!! */
! tupleid = &tuple_ctid;
! }
}
/*
*************** ExecModifyTable(ModifyTableState *node)
*** 745,755 ****
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, planSlot,
&node->mt_epqstate, estate);
break;
default:
--- 946,956 ----
slot = ExecInsert(slot, planSlot, estate);
break;
case CMD_UPDATE:
! slot = ExecUpdate(tupleid, oldtuple, slot, planSlot,
&node->mt_epqstate, estate);
break;
case CMD_DELETE:
! slot = ExecDelete(tupleid, oldtuple, planSlot,
&node->mt_epqstate, estate);
break;
default:
*************** ExecInitModifyTable(ModifyTable *node, E
*** 833,838 ****
--- 1034,1043 ----
i = 0;
foreach(l, node->plans)
{
+ Relation resultRel = estate->es_result_relation_info->ri_RelationDesc;
+ if (resultRel->rd_rel->relkind == RELKIND_VIEW)
+ ExecCheckViewTriggers(estate->es_result_relation_info, operation);
+
subplan = (Plan *) lfirst(l);
mtstate->mt_plans[i] = ExecInitNode(subplan, estate, eflags);
estate->es_result_relation_info++;
*************** ExecInitModifyTable(ModifyTable *node, E
*** 988,997 ****
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /* For UPDATE/DELETE, find the ctid junk attr now */
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
}
resultRelInfo->ri_junkFilter = j;
--- 1193,1216 ----
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
! /*
! * For UPDATE/DELETE of real tables, find the ctid junk
! * attr now. For VIEWs use the wholerow junk attr.
! */
! Relation resultRel = resultRelInfo->ri_RelationDesc;
!
! if (resultRel->rd_rel->relkind == RELKIND_VIEW)
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk wholerow column");
! }
! else
! {
! j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
! if (!AttributeNumberIsValid(j->jf_junkAttNo))
! elog(ERROR, "could not find junk ctid column");
! }
}
resultRelInfo->ri_junkFilter = j;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 5bd0ef0..b59deec 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyCreateTrigStmt(CreateTrigStmt *from
*** 3244,3249 ****
--- 3244,3250 ----
COPY_NODE_FIELD(funcname);
COPY_NODE_FIELD(args);
COPY_SCALAR_FIELD(before);
+ COPY_SCALAR_FIELD(instead);
COPY_SCALAR_FIELD(row);
COPY_SCALAR_FIELD(events);
COPY_NODE_FIELD(columns);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index c7dd42d..1bc8df7 100644
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalCreateTrigStmt(CreateTrigStmt *a,
*** 1699,1704 ****
--- 1699,1705 ----
COMPARE_NODE_FIELD(funcname);
COMPARE_NODE_FIELD(args);
COMPARE_SCALAR_FIELD(before);
+ COMPARE_SCALAR_FIELD(instead);
COMPARE_SCALAR_FIELD(row);
COMPARE_SCALAR_FIELD(events);
COMPARE_NODE_FIELD(columns);
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 9e884cb..a1ce90b 100644
*** a/src/backend/optimizer/plan/planmain.c
--- b/src/backend/optimizer/plan/planmain.c
*************** query_planner(PlannerInfo *root, List *t
*** 179,184 ****
--- 179,193 ----
*/
add_base_rels_to_query(root, (Node *) parse->jointree);
+ /*
+ * If the query target is a VIEW, it won't be in the jointree, but we
+ * need a dummy RelOptInfo node for it. This need not have any stats in
+ * it because it always just goes at the top of the plan tree.
+ */
+ if (parse->resultRelation &&
+ root->simple_rel_array[parse->resultRelation] == NULL)
+ build_simple_rel(root, parse->resultRelation, RELOPT_OTHER_MEMBER_REL);
+
/*
* Examine the targetlist and join tree, adding entries to baserel
* targetlists for all referenced Vars, and generating PlaceHolderInfo
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index a8464a7..39aab11 100644
*** a/src/backend/optimizer/prep/preptlist.c
--- b/src/backend/optimizer/prep/preptlist.c
***************
*** 38,44 ****
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table);
/*
--- 38,44 ----
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel);
/*
*************** preprocess_targetlist(PlannerInfo *root,
*** 52,57 ****
--- 52,58 ----
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
+ bool result_is_view = false;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
*************** preprocess_targetlist(PlannerInfo *root,
*** 63,81 ****
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
}
/*
! * for heap_form_tuple to work, the targetlist must match the exact order
! * of the attributes. We also need to fill in any missing attributes. -ay
! * 10/94
*/
! if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
! tlist = expand_targetlist(tlist, command_type,
! result_relation, range_table);
/*
* for "update" and "delete" queries, add ctid of the result relation into
--- 64,100 ----
if (result_relation)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
+ Relation rel;
if (rte->subquery != NULL || rte->relid == InvalidOid)
elog(ERROR, "subquery cannot be result relation");
+
+ /*
+ * Open the result relation. We assume that the rewriter already
+ * acquired at least AccessShareLock on it, so we need no lock here.
+ */
+ rel = heap_open(getrelid(result_relation, range_table), NoLock);
+ result_is_view = rel->rd_rel->relkind == RELKIND_VIEW;
+
+ /*
+ * for heap_form_tuple to work, the targetlist must match the exact
+ * order of the attributes. We also need to fill in any missing
+ * attributes. -ay 10/94
+ */
+ if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ tlist = expand_targetlist(tlist, command_type,
+ result_relation, rel);
+
+ heap_close(rel, NoLock);
}
/*
! * For an UPDATE, expand_targetlist already created a fresh tlist. For
! * DELETE, better do a listCopy so that we don't destructively modify
! * the original tlist (is this really necessary?).
*/
! if (command_type == CMD_DELETE)
! tlist = list_copy(tlist);
/*
* for "update" and "delete" queries, add ctid of the result relation into
*************** preprocess_targetlist(PlannerInfo *root,
*** 83,90 ****
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
*/
! if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
{
TargetEntry *tle;
Var *var;
--- 102,114 ----
* ExecutePlan() will be able to identify the right tuple to replace or
* delete. This extra field is marked "junk" so that it is not stored
* back into the tuple.
+ *
+ * We don't do this if the result relation is a view, since that won't
+ * expose a ctid. The rewriter should have already added a wholerow TLE
+ * for the view's subselect.
*/
! if (!result_is_view &&
! (command_type == CMD_UPDATE || command_type == CMD_DELETE))
{
TargetEntry *tle;
Var *var;
*************** preprocess_targetlist(PlannerInfo *root,
*** 97,110 ****
pstrdup("ctid"),
true);
- /*
- * For an UPDATE, expand_targetlist already created a fresh tlist. For
- * DELETE, better do a listCopy so that we don't destructively modify
- * the original tlist (is this really necessary?).
- */
- if (command_type == CMD_DELETE)
- tlist = list_copy(tlist);
-
tlist = lappend(tlist, tle);
}
--- 121,126 ----
*************** preprocess_targetlist(PlannerInfo *root,
*** 169,187 ****
}
else
{
! /* Not a table, so we need the whole row as a junk var */
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
}
}
--- 185,227 ----
}
else
{
! bool exists = false;
! ListCell *l;
!
! /*
! * Not a table, so we need the whole row as a junk var. If the
! * query target is a view, the rewriter will have already added
! * a whole row var, so don't add another for that RTE.
! */
! foreach(l, tlist)
! {
! TargetEntry *tle = (TargetEntry *) lfirst(l);
!
! if (tle->resjunk &&
! IsA(tle->expr, Var) &&
! ((Var *) tle->expr)->varno == rc->rti &&
! strcmp(tle->resname, "wholerow") == 0)
! {
! exists = true;
! break;
! }
! }
!
! if (!exists)
! {
! var = makeVar(rc->rti,
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
! snprintf(resname, sizeof(resname), "wholerow%u", rc->rti);
! tle = makeTargetEntry((Expr *) var,
! list_length(tlist) + 1,
! pstrdup(resname),
! true);
! tlist = lappend(tlist, tle);
! rc->wholeAttNo = tle->resno;
! }
}
}
*************** preprocess_targetlist(PlannerInfo *root,
*** 241,251 ****
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
- Relation rel;
int attrno,
numattrs;
--- 281,290 ----
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, Relation rel)
{
List *new_tlist = NIL;
ListCell *tlist_item;
int attrno,
numattrs;
*************** expand_targetlist(List *tlist, int comma
*** 256,267 ****
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order. We assume
! * that the rewriter already acquired at least AccessShareLock on the
! * relation, so we need no lock here.
*/
- rel = heap_open(getrelid(result_relation, range_table), NoLock);
-
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
--- 295,302 ----
* order; but we have to insert TLEs for any missing attributes.
*
* Scan the tuple description in the relation's relcache entry to make
! * sure we have all the user attributes in the right order.
*/
numattrs = RelationGetNumberOfAttributes(rel);
for (attrno = 1; attrno <= numattrs; attrno++)
*************** expand_targetlist(List *tlist, int comma
*** 399,406 ****
tlist_item = lnext(tlist_item);
}
- heap_close(rel, NoLock);
-
return new_tlist;
}
--- 434,439 ----
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index ad71d3a..16cbea2 100644
*** a/src/backend/optimizer/util/plancat.c
--- b/src/backend/optimizer/util/plancat.c
*************** estimate_rel_size(Relation rel, int32 *a
*** 441,447 ****
*tuples = 1;
break;
default:
! /* else it has no disk storage; probably shouldn't get here? */
*pages = 0;
*tuples = 0;
break;
--- 441,452 ----
*tuples = 1;
break;
default:
! /*
! * Else it has no disk storage; probably shouldn't get here,
! * unless this is a VIEW which is the query target, in which
! * case we don't care about the sizes, since it will always be
! * at the top of the plan tree.
! */
*pages = 0;
*tuples = 0;
break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4054cb1..9277f94 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** static RangeVar *makeRangeVarFromAnyName
*** 246,252 ****
%type OptSchemaName
%type OptSchemaEltList
! %type TriggerActionTime TriggerForSpec opt_trusted opt_restart_seqs
%type TriggerEvents TriggerOneEvent
%type TriggerFuncArg
--- 246,253 ----
%type OptSchemaName
%type OptSchemaEltList
! %type TriggerForSpec opt_trusted opt_restart_seqs
! %type TriggerActionTime
%type TriggerEvents TriggerOneEvent
%type TriggerFuncArg
*************** CreateTrigStmt:
*** 3448,3454 ****
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = $4;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
--- 3449,3456 ----
n->relation = $7;
n->funcname = $12;
n->args = $14;
! n->before = ($4 & 1) != 0;
! n->instead = ($4 & 2) != 0;
n->row = $8;
n->events = intVal(linitial($5));
n->columns = (List *) lsecond($5);
*************** CreateTrigStmt:
*** 3470,3475 ****
--- 3472,3478 ----
n->funcname = $17;
n->args = $19;
n->before = FALSE;
+ n->instead = FALSE;
n->row = TRUE;
n->events = intVal(linitial($6));
n->columns = (List *) lsecond($6);
*************** CreateTrigStmt:
*** 3483,3490 ****
;
TriggerActionTime:
! BEFORE { $$ = TRUE; }
! | AFTER { $$ = FALSE; }
;
TriggerEvents:
--- 3486,3494 ----
;
TriggerActionTime:
! BEFORE { $$ = 1; }
! | AFTER { $$ = 0; }
! | INSTEAD OF { $$ = 2; }
;
TriggerEvents:
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index e917554..1c8d627 100644
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** adjustJoinTreeList(Query *parsetree, boo
*** 564,570 ****
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
--- 564,572 ----
* planner will later insert NULLs for them, but there's no reason to slow
* down rewriter processing with extra tlist nodes.) Also, for both INSERT
* and UPDATE, replace explicit DEFAULT specifications with column default
! * expressions. For an UPDATE on a VIEW, add tlist entries assigning any
! * unassigned attributes their current values. The RHS of such assignments
! * will be rewritten to refer to the view's subselect node.
*
* 2. Merge multiple entries for the same target attribute, or declare error
* if we can't. Multiple entries are only allowed for INSERT/UPDATE of
*************** rewriteTargetList(Query *parsetree, Rela
*** 724,729 ****
--- 726,753 ----
false);
}
+ /*
+ * For an UPDATE on a VIEW where the TLE is missing, assign the
+ * current value, which will be rewritten as a query from the view's
+ * subselect node by ApplyRetrieveRule().
+ */
+ if (new_tle == NULL && commandType == CMD_UPDATE &&
+ target_relation->rd_rel->relkind == RELKIND_VIEW)
+ {
+ Node *new_expr;
+
+ new_expr = (Node *) makeVar(parsetree->resultRelation,
+ attrno,
+ att_tup->atttypid,
+ att_tup->atttypmod,
+ 0);
+
+ new_tle = makeTargetEntry((Expr *) new_expr,
+ attrno,
+ pstrdup(NameStr(att_tup->attname)),
+ false);
+ }
+
if (new_tle)
new_tlist = lappend(new_tlist, new_tle);
}
*************** ApplyRetrieveRule(Query *parsetree,
*** 1149,1154 ****
--- 1173,1180 ----
RangeTblEntry *rte,
*subrte;
RowMarkClause *rc;
+ Var *var;
+ TargetEntry *tle;
if (list_length(rule->actions) != 1)
elog(ERROR, "expected just one rule action");
*************** ApplyRetrieveRule(Query *parsetree,
*** 1177,1220 ****
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! /*
! * VIEWs are really easy --- just plug the view query in as a subselect,
! * replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! /*
! * We move the view's permission check data down to its rangetable. The
! * checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as implicit
! * FOR UPDATE/SHARE, the same as the parser would have done if the view's
! * subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will be
! * made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
return parsetree;
}
--- 1203,1313 ----
*/
rule_action = fireRIRrules(rule_action, activeRIRs, forUpdatePushedDown);
! if (rt_index == parsetree->resultRelation)
! {
! /*
! * We have a VIEW as the query target. For DELETE and UPDATE, we
! * add a new subselect RTE using the view's query and adjust any
! * VARs in the original query to point to this instead of the
! * original view RTE.
! *
! * We keep the original view in the rtable as the query target, and
! * any Vars in the returning list that reference it are left alone.
! *
! * The resulting jointree fromlist will not refer to the view RTE,
! * and so the planner won't try to join to it. This will result in
! * a plan with a ModifyTable node at the root, referring to the
! * original view relation, and a subselect based on the view's query
! * merged with any user conditions.
! *
! * For INSERTS we do nothing. The original view remains the query
! * target.
! */
! if (parsetree->commandType == CMD_DELETE ||
! parsetree->commandType == CMD_UPDATE)
! {
! List *returningList = parsetree->returningList;
! /*
! * Make a new subselect RTE from the view query and adjust the
! * the original query to point to this instead of the original
! * view, while preserving the view resultRelation and any
! * returningList Vars.
! */
! rte = copyObject(rt_fetch(rt_index, parsetree->rtable));
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
! parsetree->rtable = lappend(parsetree->rtable, rte);
! parsetree->returningList = NIL;
! ChangeVarNodes((Node *) parsetree, rt_index,
! list_length(parsetree->rtable), 0);
!
! parsetree->resultRelation = rt_index;
! parsetree->returningList = returningList;
!
! /*
! * Add a "wholerow" junk TLE so that the executor can retrieve
! * the old VIEW tuples to pass to the INSTEAD OF triggers.
! */
! var = makeVar(list_length(parsetree->rtable),
! InvalidAttrNumber,
! RECORDOID,
! -1,
! 0);
!
! tle = makeTargetEntry((Expr *) var,
! list_length(parsetree->targetList) + 1,
! pstrdup("wholerow"),
! true);
! parsetree->targetList = lappend(parsetree->targetList, tle);
! }
! }
! else
! {
! /*
! * Selecting from the VIEW --- just plug the view query in as a
! * subselect, replacing the relation's original RTE.
! */
! rte = rt_fetch(rt_index, parsetree->rtable);
!
! rte->rtekind = RTE_SUBQUERY;
! rte->relid = InvalidOid;
! rte->subquery = rule_action;
! rte->inh = false; /* must not be set for a subquery */
!
! /*
! * We move the view's permission check data down to its rangetable.
! * The checks will actually be done against the OLD entry therein.
! */
! subrte = rt_fetch(PRS2_OLD_VARNO, rule_action->rtable);
! Assert(subrte->relid == relation->rd_id);
! subrte->requiredPerms = rte->requiredPerms;
! subrte->checkAsUser = rte->checkAsUser;
! subrte->selectedCols = rte->selectedCols;
! subrte->modifiedCols = rte->modifiedCols;
!
! rte->requiredPerms = 0; /* no permission check on subquery itself */
! rte->checkAsUser = InvalidOid;
! rte->selectedCols = NULL;
! rte->modifiedCols = NULL;
!
! /*
! * If FOR UPDATE/SHARE of view, mark all the contained tables as
! * implicit FOR UPDATE/SHARE, the same as the parser would have done
! * if the view's subquery had been written out explicitly.
! *
! * Note: we don't consider forUpdatePushedDown here; such marks will
! * be made by recursing from the upper level in markQueryForLocking.
! */
! if (rc != NULL)
! markQueryForLocking(rule_action, (Node *) rule_action->jointree,
! rc->forUpdate, rc->noWait, true);
! }
return parsetree;
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 578b9ce..6cbbada 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** pg_get_triggerdef_worker(Oid trigid, boo
*** 545,550 ****
--- 545,552 ----
if (TRIGGER_FOR_BEFORE(trigrec->tgtype))
appendStringInfo(&buf, "BEFORE");
+ else if (TRIGGER_FOR_INSTEAD(trigrec->tgtype))
+ appendStringInfo(&buf, "INSTEAD OF");
else
appendStringInfo(&buf, "AFTER");
if (TRIGGER_FOR_INSERT(trigrec->tgtype))
diff --git a/src/backend/utils/adt/tsvector_op.c b/src/backend/utils/adt/tsvector_op.c
index 399fee8..75687aa 100644
*** a/src/backend/utils/adt/tsvector_op.c
--- b/src/backend/utils/adt/tsvector_op.c
*************** tsvector_update_trigger(PG_FUNCTION_ARGS
*** 1259,1265 ****
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
--- 1259,1265 ----
trigdata = (TriggerData *) fcinfo->context;
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: can't process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "tsvector_update_trigger: must be fired BEFORE event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ff7d97a..08a16df 100644
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
*************** dumpTrigger(Archive *fout, TriggerInfo *
*** 12099,12104 ****
--- 12099,12106 ----
findx = 0;
if (TRIGGER_FOR_BEFORE(tginfo->tgtype))
appendPQExpBuffer(query, "BEFORE");
+ if (TRIGGER_FOR_INSTEAD(tginfo->tgtype))
+ appendPQExpBuffer(query, "INSTEAD OF");
else
appendPQExpBuffer(query, "AFTER");
if (TRIGGER_FOR_INSERT(tginfo->tgtype))
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 83e9845..70dc7be 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** describeOneTableDetails(const char *sche
*** 1820,1827 ****
--- 1820,1838 ----
}
PQclear(result);
}
+ }
+ /*
+ * Print triggers next.
+ * This is part of the footer information about a table, but may also
+ * apply to a view.
+ */
+ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v')
+ {
/* print triggers (but only user-defined triggers) */
+ PGresult *result = NULL;
+ int tuples = 0;
+
if (tableinfo.hastriggers)
{
printfPQExpBuffer(&buf,
*************** describeOneTableDetails(const char *sche
*** 1935,1942 ****
--- 1946,1963 ----
}
PQclear(result);
}
+ }
+ /*
+ * Done printing trigger information for view or table.
+ * Finish printing the footer information about a table.
+ */
+ if (tableinfo.relkind == 'r')
+ {
/* print inherited tables */
+ PGresult *result = NULL;
+ int tuples = 0;
+
printfPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i WHERE c.oid=i.inhparent AND i.inhrelid = '%s' ORDER BY inhseqno", oid);
result = PSQLexec(buf.data, false);
diff --git a/src/include/catalog/pg_trigger.h b/src/include/catalog/pg_trigger.h
index 46285ac..8eaeacf 100644
*** a/src/include/catalog/pg_trigger.h
--- b/src/include/catalog/pg_trigger.h
*************** typedef FormData_pg_trigger *Form_pg_tri
*** 91,96 ****
--- 91,97 ----
#define TRIGGER_TYPE_DELETE (1 << 3)
#define TRIGGER_TYPE_UPDATE (1 << 4)
#define TRIGGER_TYPE_TRUNCATE (1 << 5)
+ #define TRIGGER_TYPE_INSTEAD (1 << 6)
/* Macros for manipulating tgtype */
#define TRIGGER_CLEAR_TYPE(type) ((type) = 0)
*************** typedef FormData_pg_trigger *Form_pg_tri
*** 101,106 ****
--- 102,108 ----
#define TRIGGER_SETT_DELETE(type) ((type) |= TRIGGER_TYPE_DELETE)
#define TRIGGER_SETT_UPDATE(type) ((type) |= TRIGGER_TYPE_UPDATE)
#define TRIGGER_SETT_TRUNCATE(type) ((type) |= TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_SETT_INSTEAD(type) ((type) |= TRIGGER_TYPE_INSTEAD)
#define TRIGGER_FOR_ROW(type) ((type) & TRIGGER_TYPE_ROW)
#define TRIGGER_FOR_BEFORE(type) ((type) & TRIGGER_TYPE_BEFORE)
*************** typedef FormData_pg_trigger *Form_pg_tri
*** 108,112 ****
--- 110,115 ----
#define TRIGGER_FOR_DELETE(type) ((type) & TRIGGER_TYPE_DELETE)
#define TRIGGER_FOR_UPDATE(type) ((type) & TRIGGER_TYPE_UPDATE)
#define TRIGGER_FOR_TRUNCATE(type) ((type) & TRIGGER_TYPE_TRUNCATE)
+ #define TRIGGER_FOR_INSTEAD(type) ((type) & TRIGGER_TYPE_INSTEAD)
#endif /* PG_TRIGGER_H */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 08bb22a..d94c8c7 100644
*** a/src/include/commands/trigger.h
--- b/src/include/commands/trigger.h
*************** typedef struct TriggerData
*** 53,58 ****
--- 53,59 ----
#define TRIGGER_EVENT_OPMASK 0x00000003
#define TRIGGER_EVENT_ROW 0x00000004
#define TRIGGER_EVENT_BEFORE 0x00000008
+ #define TRIGGER_EVENT_INSTEAD 0x00000040
/* More TriggerEvent flags, used only within trigger.c */
*************** typedef struct TriggerData
*** 84,91 ****
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event))
/*
* Definitions for the replication role based firing.
--- 85,95 ----
#define TRIGGER_FIRED_BEFORE(event) \
((TriggerEvent) (event) & TRIGGER_EVENT_BEFORE)
+ #define TRIGGER_FIRED_INSTEAD(event) \
+ ((TriggerEvent) (event) & TRIGGER_EVENT_INSTEAD)
+
#define TRIGGER_FIRED_AFTER(event) \
! (!TRIGGER_FIRED_BEFORE (event) && !TRIGGER_FIRED_INSTEAD (event))
/*
* Definitions for the replication role based firing.
*************** extern void ExecARInsertTriggers(EState
*** 135,140 ****
--- 139,147 ----
ResultRelInfo *relinfo,
HeapTuple trigtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRInsertTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSDeleteTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASDeleteTriggers(EState *estate,
*************** extern bool ExecBRDeleteTriggers(EState
*** 146,151 ****
--- 153,161 ----
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid);
+ extern bool ExecIRDeleteTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple trigtuple);
extern void ExecBSUpdateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASUpdateTriggers(EState *estate,
*************** extern void ExecARUpdateTriggers(EState
*** 160,165 ****
--- 170,179 ----
ItemPointer tupleid,
HeapTuple newtuple,
List *recheckIndexes);
+ extern HeapTuple ExecIRUpdateTriggers(EState *estate,
+ ResultRelInfo *relinfo,
+ HeapTuple oldtuple,
+ HeapTuple newtuple);
extern void ExecBSTruncateTriggers(EState *estate,
ResultRelInfo *relinfo);
extern void ExecASTruncateTriggers(EState *estate,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b2f0fef..4fdd001 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct CreateTrigStmt
*** 1609,1614 ****
--- 1609,1615 ----
List *funcname; /* qual. name of function to call */
List *args; /* list of (T_String) Values or NIL */
bool before; /* BEFORE/AFTER */
+ bool instead; /* INSTEAD OF (overrides BEFORE/AFTER) */
bool row; /* ROW/STATEMENT */
/* events uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */
int16 events; /* INSERT/UPDATE/DELETE/TRUNCATE */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 17ad888..ea8f78b 100644
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
*************** typedef struct TriggerDesc
*** 82,91 ****
--- 82,93 ----
uint16 n_before_statement[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_before_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ uint16 n_instead_row[TRIGGER_NUM_EVENT_CLASSES];
uint16 n_after_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_statement[TRIGGER_NUM_EVENT_CLASSES];
int *tg_before_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_row[TRIGGER_NUM_EVENT_CLASSES];
+ int *tg_instead_row[TRIGGER_NUM_EVENT_CLASSES];
int *tg_after_statement[TRIGGER_NUM_EVENT_CLASSES];
/* The actual array of triggers is here */
diff --git a/src/pl/plperl/expected/plperl_trigger.out b/src/pl/plperl/expected/plperl_trigger.out
index b5af566..8cd74cb 100644
*** a/src/pl/plperl/expected/plperl_trigger.out
--- b/src/pl/plperl/expected/plperl_trigger.out
*************** NOTICE: $_TD->{when} = 'BEFORE'
*** 124,129 ****
--- 124,207 ----
CONTEXT: PL/Perl function "trigger_data"
DROP TRIGGER show_trigger_data_trig on trigger_test;
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'INSERT'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{new} = {'i' => '2', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ update trigger_test_view set v = 'update' where i = 1;
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'UPDATE'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{new} = {'i' => '1', 'v' => 'update'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ delete from trigger_test_view;
+ NOTICE: $_TD->{argc} = '2'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{args} = ['24', 'skidoo view']
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{event} = 'DELETE'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{level} = 'ROW'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{name} = 'show_trigger_data_trig'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{old} = {'i' => '1', 'v' => 'insert'}
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relid} = 'bogus:12345'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{relname} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_name} = 'trigger_test_view'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{table_schema} = 'public'
+ CONTEXT: PL/Perl function "trigger_data"
+ NOTICE: $_TD->{when} = 'INSTEAD OF'
+ CONTEXT: PL/Perl function "trigger_data"
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
DROP FUNCTION trigger_data();
CREATE OR REPLACE FUNCTION valid_id() RETURNS trigger AS $$
diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index cfad487..bb52bd6 100644
*** a/src/pl/plperl/plperl.c
--- b/src/pl/plperl/plperl.c
*************** plperl_trigger_build_args(FunctionCallIn
*** 954,959 ****
--- 954,961 ----
when = "BEFORE";
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
when = "AFTER";
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ when = "INSTEAD OF";
else
when = "UNKNOWN";
hv_store_string(hv, "when", newSVstring(when));
diff --git a/src/pl/plperl/sql/plperl_trigger.sql b/src/pl/plperl/sql/plperl_trigger.sql
index d55f79a..1e68759 100644
*** a/src/pl/plperl/sql/plperl_trigger.sql
--- b/src/pl/plperl/sql/plperl_trigger.sql
*************** update trigger_test set v = 'update' whe
*** 60,65 ****
--- 60,79 ----
delete from trigger_test;
DROP TRIGGER show_trigger_data_trig on trigger_test;
+
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i = 1;
+ delete from trigger_test_view;
+
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
DROP FUNCTION trigger_data();
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 6601320..1dc72a0 100644
*** a/src/pl/plpgsql/src/pl_exec.c
--- b/src/pl/plpgsql/src/pl_exec.c
*************** plpgsql_exec_trigger(PLpgSQL_function *f
*** 581,588 ****
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE or AFTER");
var->isnull = false;
var->freeval = true;
--- 581,590 ----
var->value = CStringGetTextDatum("BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
var->value = CStringGetTextDatum("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ var->value = CStringGetTextDatum("INSTEAD OF");
else
! elog(ERROR, "unrecognized trigger execution time: not BEFORE, AFTER or INSTEAD OF");
var->isnull = false;
var->freeval = true;
diff --git a/src/pl/plpython/expected/plpython_trigger.out b/src/pl/plpython/expected/plpython_trigger.out
index 3192ff1..275d0f7 100644
*** a/src/pl/plpython/expected/plpython_trigger.out
--- b/src/pl/plpython/expected/plpython_trigger.out
*************** NOTICE: TD[table_schema] => public
*** 294,304 ****
CONTEXT: PL/Python function "trigger_data"
NOTICE: TD[when] => BEFORE
CONTEXT: PL/Python function "trigger_data"
DROP FUNCTION trigger_data() CASCADE;
! NOTICE: drop cascades to 3 other objects
! DETAIL: drop cascades to trigger show_trigger_data_trig_before on table trigger_test
! drop cascades to trigger show_trigger_data_trig_after on table trigger_test
! drop cascades to trigger show_trigger_data_trig_stmt on table trigger_test
--
-- trigger error handling
--
--- 294,374 ----
CONTEXT: PL/Python function "trigger_data"
NOTICE: TD[when] => BEFORE
CONTEXT: PL/Python function "trigger_data"
+ DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
+ DROP TRIGGER show_trigger_data_trig_before on trigger_test;
+ DROP TRIGGER show_trigger_data_trig_after on trigger_test;
+ insert into trigger_test values(1,'insert');
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+ CREATE TRIGGER show_trigger_data_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => INSERT
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => {'i': 2, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => None
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
+ update trigger_test_view set v = 'update' where i = 1;
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => UPDATE
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => {'i': 1, 'v': 'update'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
+ delete from trigger_test_view;
+ NOTICE: TD[args] => ['24', 'skidoo view']
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[event] => DELETE
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[level] => ROW
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[name] => show_trigger_data_trig
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[new] => None
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[relid] => bogus:12345
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_name] => trigger_test_view
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[table_schema] => public
+ CONTEXT: PL/Python function "trigger_data"
+ NOTICE: TD[when] => INSTEAD OF
+ CONTEXT: PL/Python function "trigger_data"
DROP FUNCTION trigger_data() CASCADE;
! NOTICE: drop cascades to trigger show_trigger_data_trig on view trigger_test_view
! DROP VIEW trigger_test_view;
! delete from trigger_test;
--
-- trigger error handling
--
diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c
index 6e35637..1d512e0 100644
*** a/src/pl/plpython/plpython.c
--- b/src/pl/plpython/plpython.c
*************** PLy_trigger_build_args(FunctionCallInfo
*** 846,851 ****
--- 846,853 ----
pltwhen = PyString_FromString("BEFORE");
else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
pltwhen = PyString_FromString("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ pltwhen = PyString_FromString("INSTEAD OF");
else
{
elog(ERROR, "unrecognized WHEN tg_event: %u", tdata->tg_event);
diff --git a/src/pl/plpython/sql/plpython_trigger.sql b/src/pl/plpython/sql/plpython_trigger.sql
index c60a673..fba4db0 100644
*** a/src/pl/plpython/sql/plpython_trigger.sql
--- b/src/pl/plpython/sql/plpython_trigger.sql
*************** update trigger_test set v = 'update' whe
*** 99,106 ****
delete from trigger_test;
truncate table trigger_test;
! DROP FUNCTION trigger_data() CASCADE;
--
-- trigger error handling
--- 99,122 ----
delete from trigger_test;
truncate table trigger_test;
! DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
! DROP TRIGGER show_trigger_data_trig_before on trigger_test;
! DROP TRIGGER show_trigger_data_trig_after on trigger_test;
!
! insert into trigger_test values(1,'insert');
! CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
!
! CREATE TRIGGER show_trigger_data_trig
! INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
! FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i = 1;
+ delete from trigger_test_view;
+
+ DROP FUNCTION trigger_data() CASCADE;
+ DROP VIEW trigger_test_view;
+ delete from trigger_test;
--
-- trigger error handling
diff --git a/src/pl/tcl/expected/pltcl_queries.out b/src/pl/tcl/expected/pltcl_queries.out
index cdab74a..b585736 100644
*** a/src/pl/tcl/expected/pltcl_queries.out
--- b/src/pl/tcl/expected/pltcl_queries.out
*************** NOTICE: TG_table_name: trigger_test
*** 196,201 ****
--- 196,237 ----
NOTICE: TG_table_schema: public
NOTICE: TG_when: BEFORE
NOTICE: args: {23 skidoo}
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: NEW: {i: 2, v: insert}
+ NOTICE: OLD: {}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: INSERT
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ update trigger_test_view set v = 'update' where i=1;
+ NOTICE: NEW: {i: 1, v: update}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: UPDATE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ delete from trigger_test_view;
+ NOTICE: NEW: {}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: DELETE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
update trigger_test set v = 'update' where i = 1;
NOTICE: NEW: {i: 1, v: update}
NOTICE: OLD: {i: 1, v: insert}
diff --git a/src/pl/tcl/expected/pltcl_queries_1.out b/src/pl/tcl/expected/pltcl_queries_1.out
index 5788df7..e3fd24d 100644
*** a/src/pl/tcl/expected/pltcl_queries_1.out
--- b/src/pl/tcl/expected/pltcl_queries_1.out
*************** NOTICE: TG_table_name: trigger_test
*** 196,201 ****
--- 196,237 ----
NOTICE: TG_table_schema: public
NOTICE: TG_when: BEFORE
NOTICE: args: {23 skidoo}
+ insert into trigger_test_view values(2,'insert');
+ NOTICE: NEW: {i: 2, v: insert}
+ NOTICE: OLD: {}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: INSERT
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ update trigger_test_view set v = 'update' where i=1;
+ NOTICE: NEW: {i: 1, v: update}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: UPDATE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
+ delete from trigger_test_view;
+ NOTICE: NEW: {}
+ NOTICE: OLD: {i: 1, v: insert}
+ NOTICE: TG_level: ROW
+ NOTICE: TG_name: show_trigger_data_view_trig
+ NOTICE: TG_op: DELETE
+ NOTICE: TG_relatts: {{} i v}
+ NOTICE: TG_relid: bogus:12345
+ NOTICE: TG_table_name: trigger_test_view
+ NOTICE: TG_table_schema: public
+ NOTICE: TG_when: {INSTEAD OF}
+ NOTICE: args: {24 {skidoo view}}
update trigger_test set v = 'update' where i = 1;
NOTICE: NEW: {i: 1, v: update}
NOTICE: OLD: {i: 1, v: insert}
diff --git a/src/pl/tcl/expected/pltcl_setup.out b/src/pl/tcl/expected/pltcl_setup.out
index e46c1c3..f577e66 100644
*** a/src/pl/tcl/expected/pltcl_setup.out
--- b/src/pl/tcl/expected/pltcl_setup.out
*************** create function check_pkey1_exists(int4,
*** 51,56 ****
--- 51,57 ----
-- dump trigger data
CREATE TABLE trigger_test
(i int, v text );
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
if { [info exists TG_relid] } {
*************** $_$;
*** 85,90 ****
--- 86,94 ----
CREATE TRIGGER show_trigger_data_trig
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ CREATE TRIGGER show_trigger_data_view_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
--
-- Trigger function on every change to T_pkey1
--
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 8c94c82..7bf79fa 100644
*** a/src/pl/tcl/pltcl.c
--- b/src/pl/tcl/pltcl.c
*************** pltcl_trigger_handler(PG_FUNCTION_ARGS)
*** 822,827 ****
--- 822,829 ----
Tcl_DStringAppendElement(&tcl_cmd, "BEFORE");
else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
Tcl_DStringAppendElement(&tcl_cmd, "AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+ Tcl_DStringAppendElement(&tcl_cmd, "INSTEAD OF");
else
elog(ERROR, "unrecognized WHEN tg_event: %u", trigdata->tg_event);
diff --git a/src/pl/tcl/sql/pltcl_queries.sql b/src/pl/tcl/sql/pltcl_queries.sql
index 3a99132..8dbb6ec 100644
*** a/src/pl/tcl/sql/pltcl_queries.sql
--- b/src/pl/tcl/sql/pltcl_queries.sql
*************** select * from T_pkey2 order by key1 usin
*** 76,80 ****
--- 76,85 ----
-- show dump of trigger data
insert into trigger_test values(1,'insert');
+
+ insert into trigger_test_view values(2,'insert');
+ update trigger_test_view set v = 'update' where i=1;
+ delete from trigger_test_view;
+
update trigger_test set v = 'update' where i = 1;
delete from trigger_test;
diff --git a/src/pl/tcl/sql/pltcl_setup.sql b/src/pl/tcl/sql/pltcl_setup.sql
index 4a581ed..a9370d2 100644
*** a/src/pl/tcl/sql/pltcl_setup.sql
--- b/src/pl/tcl/sql/pltcl_setup.sql
*************** create function check_pkey1_exists(int4,
*** 60,65 ****
--- 60,67 ----
CREATE TABLE trigger_test
(i int, v text );
+ CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
if { [info exists TG_relid] } {
*************** CREATE TRIGGER show_trigger_data_trig
*** 96,101 ****
--- 98,106 ----
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ CREATE TRIGGER show_trigger_data_view_trig
+ INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
--
-- Trigger function on every change to T_pkey1
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index fe98079..36d632f 100644
*** a/src/test/regress/expected/triggers.out
--- b/src/test/regress/expected/triggers.out
*************** SELECT * FROM min_updates_test_oids;
*** 791,793 ****
--- 791,1419 ----
DROP TABLE min_updates_test;
DROP TABLE min_updates_test_oids;
+ --
+ -- Test triggers on views
+ --
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ ERROR: cannot insert into a view
+ HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+ UPDATE main_view SET b = 20 WHERE a = 50;
+ ERROR: cannot update a view
+ HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+ DELETE FROM main_view WHERE a = 50;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a = 51;
+ ERROR: cannot delete from a view
+ HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+ -- VIEW trigger function
+ CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
+ LANGUAGE plpgsql AS $$
+ declare
+ argstr text := '';
+ begin
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ raise notice '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr;
+
+ if TG_WHEN = 'INSTEAD OF' then
+ if TG_OP = 'INSERT' then
+ raise NOTICE 'NEW: %', NEW;
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ raise NOTICE 'OLD: %, NEW: %', OLD, NEW;
+ UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ raise NOTICE 'OLD: %', OLD;
+ DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end if;
+
+ RETURN NULL;
+ end;
+ $$;
+ -- Before row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ -- After row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+ ERROR: "main_view" is a view
+ HINT: Define ROW-level BEFORE and AFTER triggers on tables
+ -- Truncate triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+ ERROR: "main_view" is a view
+ HINT: Define TRUNCATE triggers on tables
+ -- INSTEAD OF triggers aren't allowed on tables
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+ ERROR: "main_table" is a table
+ HINT: Define INSTEAD OF triggers on views
+ -- Don't support WHEN clauses with INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: cannot use a WHEN condition with an INSTEAD OF trigger
+ -- Don't support column-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: cannot use a column list with an INSTEAD OF trigger
+ -- Don't support statement-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ ERROR: STATEMENT-level INSTEAD OF triggers are not supported
+ HINT: Use FOR EACH ROW for INSTEAD OF triggers
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+ -- Valid BEFORE statement VIEW triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
+ CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt');
+ CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt');
+ -- Valid AFTER statement VIEW triggers
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt');
+ CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt');
+ CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt');
+ \set QUIET false
+ -- Insert into view using trigger
+ INSERT INTO main_view VALUES (20, 30);
+ NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt)
+ NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins)
+ NOTICE: NEW: (20,30)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt)
+ INSERT 0 1
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+ NOTICE: main_view BEFORE INSERT STATEMENT (before_view_ins_stmt)
+ NOTICE: main_view INSTEAD OF INSERT ROW (instead_of_ins)
+ NOTICE: NEW: (21,31)
+ NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "INSERT INTO main_table VALUES (NEW.a, NEW.b)"
+ PL/pgSQL function "view_trigger" line 17 at SQL statement
+ NOTICE: main_view AFTER INSERT STATEMENT (after_view_ins_stmt)
+ a | b
+ ----+----
+ 21 | 31
+ (1 row)
+
+ INSERT 0 1
+ -- Table trigger will prevent updates
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (20,30), NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 0
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (21,31), NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ a | b
+ ---+---
+ (0 rows)
+
+ UPDATE 0
+ -- Remove table trigger to allow updates
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ DROP TRIGGER
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (20,30), NEW: (20,31)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 1
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view INSTEAD OF UPDATE ROW (instead_of_upd)
+ NOTICE: OLD: (21,31), NEW: (21,32)
+ NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+ CONTEXT: SQL statement "UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b"
+ PL/pgSQL function "view_trigger" line 23 at SQL statement
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ a | b
+ ----+----
+ 21 | 32
+ (1 row)
+
+ UPDATE 1
+ -- Before and after stmt triggers should fire even when no rows are affected
+ UPDATE main_view SET b = 0 WHERE false;
+ NOTICE: main_view BEFORE UPDATE STATEMENT (before_view_upd_stmt)
+ NOTICE: main_view AFTER UPDATE STATEMENT (after_view_upd_stmt)
+ UPDATE 0
+ -- Delete from view using trigger
+ DELETE FROM main_view WHERE a IN (20,21);
+ NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (21,10)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (20,31)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (21,32)
+ NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt)
+ DELETE 3
+ DELETE FROM main_view WHERE a = 31 RETURNING a, b;
+ NOTICE: main_view BEFORE DELETE STATEMENT (before_view_del_stmt)
+ NOTICE: main_view INSTEAD OF DELETE ROW (instead_of_del)
+ NOTICE: OLD: (31,10)
+ NOTICE: main_view AFTER DELETE STATEMENT (after_view_del_stmt)
+ a | b
+ ----+----
+ 31 | 10
+ (1 row)
+
+ DELETE 1
+ \set QUIET true
+ -- Describe view should list triggers
+ \d main_view
+ View "public.main_view"
+ Column | Type | Modifiers
+ --------+---------+-----------
+ a | integer |
+ b | integer |
+ Triggers:
+ after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt')
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt')
+ before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt')
+ before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt')
+ instead_of_delete_trig INSTEAD OF DELETE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del')
+ instead_of_insert_trig INSTEAD OF INSERT ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd')
+
+ -- Test dropping view triggers
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d+ main_view
+ View "public.main_view"
+ Column | Type | Modifiers | Storage | Description
+ --------+---------+-----------+---------+-------------
+ a | integer | | plain |
+ b | integer | | plain |
+ View definition:
+ SELECT main_table.a, main_table.b
+ FROM main_table;
+ Triggers:
+ after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt')
+ after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt')
+ before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt')
+ before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt')
+ instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd')
+
+ DROP VIEW main_view;
+ --
+ -- Test triggers on a join view
+ --
+ CREATE TABLE country_table (
+ country_id serial primary key,
+ country_name text unique not null,
+ continent text not null
+ );
+ NOTICE: CREATE TABLE will create implicit sequence "country_table_country_id_seq" for serial column "country_table.country_id"
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "country_table_pkey" for table "country_table"
+ NOTICE: CREATE TABLE / UNIQUE will create implicit index "country_table_country_name_key" for table "country_table"
+ INSERT INTO country_table (country_name, continent)
+ VALUES ('Japan', 'Asia'),
+ ('UK', 'Europe'),
+ ('USA', 'North America')
+ RETURNING *;
+ country_id | country_name | continent
+ ------------+--------------+---------------
+ 1 | Japan | Asia
+ 2 | UK | Europe
+ 3 | USA | North America
+ (3 rows)
+
+ CREATE TABLE city_table (
+ city_id serial primary key,
+ city_name text not null,
+ population bigint,
+ country_id int references country_table
+ );
+ NOTICE: CREATE TABLE will create implicit sequence "city_table_city_id_seq" for serial column "city_table.city_id"
+ NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "city_table_pkey" for table "city_table"
+ CREATE VIEW city_view AS
+ SELECT city_id, city_name, population, country_name, continent
+ FROM city_table ci
+ LEFT JOIN country_table co ON co.country_id = ci.country_id;
+ CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS NOT NULL then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+ else
+ NEW.continent := NULL;
+ end if;
+
+ if NEW.city_id IS NOT NULL then
+ INSERT INTO city_table
+ VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id);
+ else
+ INSERT INTO city_table(city_name, population, country_id)
+ VALUES(NEW.city_name, NEW.population, ctry_id)
+ RETURNING city_id INTO NEW.city_id;
+ end if;
+
+ RETURN NEW;
+ end;
+ $$;
+ CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_insert();
+ CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ DELETE FROM city_table WHERE city_id = OLD.city_id;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end;
+ $$;
+ CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_delete();
+ CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS DISTINCT FROM OLD.country_name then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population,
+ country_id = ctry_id
+ WHERE city_id = OLD.city_id;
+ else
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population
+ WHERE city_id = OLD.city_id;
+ NEW.continent := OLD.continent;
+ end if;
+
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end;
+ $$;
+ CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_update();
+ -- INSERT .. RETURNING
+ \set QUIET false
+ INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 2 | London | 7556900 | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+---------------+------------+--------------+---------------
+ 3 | Washington DC | | USA | North America
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 123456 | New York | | |
+ (1 row)
+
+ INSERT 0 1
+ INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ INSERT 0 1
+ -- UPDATE .. RETURNING
+ UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error
+ ERROR: No such country: "Japon"
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match
+ UPDATE 0
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | | Japan | Asia
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 1 | Tokyo | 13010279 | Japan | Asia
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 123456 | New York | | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+---------------
+ 123456 | New York | 8391881 | USA | North America
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2
+ WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *;
+ city_id | city_name | population | country_name | continent | city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------+---------+------------+------------+--------------+-----------
+ 2 | London | 7556900 | UK | Europe | 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ -- DELETE .. RETURNING
+ DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+------------+------------+--------------+-----------
+ 234567 | Birmingham | 1016800 | UK | Europe
+ (1 row)
+
+ DELETE 1
+ \set QUIET true
+ -- read-only view with WHERE clause
+ CREATE VIEW european_city_view AS
+ SELECT * FROM city_view WHERE continent = 'Europe';
+ SELECT count(*) FROM european_city_view;
+ count
+ -------
+ 1
+ (1 row)
+
+ CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql
+ AS 'begin RETURN NULL; end';
+ CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE
+ ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn();
+ \set QUIET false
+ INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z');
+ INSERT 0 0
+ UPDATE european_city_view SET population = 10000;
+ UPDATE 0
+ DELETE FROM european_city_view;
+ DELETE 0
+ \set QUIET true
+ -- rules bypassing no-op triggers
+ CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view
+ DO INSTEAD INSERT INTO city_view
+ VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent)
+ RETURNING *;
+ CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view
+ DO INSTEAD UPDATE city_view SET
+ city_name = NEW.city_name,
+ population = NEW.population,
+ country_name = NEW.country_name
+ WHERE city_id = OLD.city_id
+ RETURNING NEW.*;
+ CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view
+ DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *;
+ -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are
+ \set QUIET false
+ INSERT INTO european_city_view(city_name, country_name)
+ VALUES ('Cambridge', 'USA') RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+---------------
+ 4 | Cambridge | | USA | North America
+ (1 row)
+
+ INSERT 0 1
+ UPDATE european_city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge';
+ UPDATE 0
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge';
+ DELETE 0
+ -- UPDATE and DELETE via rule and trigger
+ UPDATE city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ UPDATE european_city_view SET population = 122800
+ WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | 122800 | UK | Europe
+ (1 row)
+
+ UPDATE 1
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *;
+ city_id | city_name | population | country_name | continent
+ ---------+-----------+------------+--------------+-----------
+ 4 | Cambridge | 122800 | UK | Europe
+ (1 row)
+
+ DELETE 1
+ -- join UPDATE test
+ UPDATE city_view v SET population = 599657
+ FROM city_table ci, country_table co
+ WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA'
+ AND v.city_id = ci.city_id AND v.country_name = co.country_name
+ RETURNING co.country_id, v.country_name,
+ v.city_id, v.city_name, v.population;
+ country_id | country_name | city_id | city_name | population
+ ------------+--------------+---------+---------------+------------
+ 3 | USA | 3 | Washington DC | 599657
+ (1 row)
+
+ UPDATE 1
+ \set QUIET true
+ SELECT * FROM city_view;
+ city_id | city_name | population | country_name | continent
+ ---------+---------------+------------+--------------+---------------
+ 1 | Tokyo | 13010279 | Japan | Asia
+ 123456 | New York | 8391881 | USA | North America
+ 2 | London | 7556900 | UK | Europe
+ 3 | Washington DC | 599657 | USA | North America
+ (4 rows)
+
+ DROP TABLE city_table CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to view city_view
+ drop cascades to view european_city_view
+ DROP TABLE country_table;
diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c
index 8b0f9f1..4b4ede0 100644
*** a/src/test/regress/regress.c
--- b/src/test/regress/regress.c
*************** ttdummy(PG_FUNCTION_ARGS)
*** 489,495 ****
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
--- 489,495 ----
elog(ERROR, "ttdummy: not fired by trigger manager");
if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process STATEMENT events");
! if (!TRIGGER_FIRED_BEFORE(trigdata->tg_event))
elog(ERROR, "ttdummy: must be fired before event");
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
elog(ERROR, "ttdummy: cannot process INSERT event");
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 68495ca..33fb7f4 100644
*** a/src/test/regress/sql/triggers.sql
--- b/src/test/regress/sql/triggers.sql
*************** SELECT * FROM min_updates_test_oids;
*** 578,580 ****
--- 578,937 ----
DROP TABLE min_updates_test;
DROP TABLE min_updates_test_oids;
+
+ --
+ -- Test triggers on views
+ --
+
+ CREATE VIEW main_view AS SELECT a, b FROM main_table;
+
+ -- Updates should fail without rules or triggers
+ INSERT INTO main_view VALUES (1,2);
+ UPDATE main_view SET b = 20 WHERE a = 50;
+ DELETE FROM main_view WHERE a = 50;
+ -- Should fail even when there are no matching rows
+ DELETE FROM main_view WHERE a = 51;
+
+ -- VIEW trigger function
+ CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
+ LANGUAGE plpgsql AS $$
+ declare
+ argstr text := '';
+ begin
+ for i in 0 .. TG_nargs - 1 loop
+ if i > 0 then
+ argstr := argstr || ', ';
+ end if;
+ argstr := argstr || TG_argv[i];
+ end loop;
+
+ raise notice '% % % % (%)', TG_RELNAME, TG_WHEN, TG_OP, TG_LEVEL, argstr;
+
+ if TG_WHEN = 'INSTEAD OF' then
+ if TG_OP = 'INSERT' then
+ raise NOTICE 'NEW: %', NEW;
+ INSERT INTO main_table VALUES (NEW.a, NEW.b);
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'UPDATE' then
+ raise NOTICE 'OLD: %, NEW: %', OLD, NEW;
+ UPDATE main_table SET a = NEW.a, b = NEW.b WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end if;
+
+ if TG_OP = 'DELETE' then
+ raise NOTICE 'OLD: %', OLD;
+ DELETE FROM main_table WHERE a = OLD.a AND b = OLD.b;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end if;
+ end if;
+
+ RETURN NULL;
+ end;
+ $$;
+
+ -- Before row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig BEFORE UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig BEFORE DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ -- After row triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig AFTER INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_ins_row');
+
+ CREATE TRIGGER invalid_trig AFTER UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_row');
+
+ CREATE TRIGGER invalid_trig AFTER DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_del_row');
+
+ -- Truncate triggers aren't allowed on views
+ CREATE TRIGGER invalid_trig BEFORE TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
+ EXECUTE PROCEDURE trigger_func('before_tru_row');
+
+ -- INSTEAD OF triggers aren't allowed on tables
+ CREATE TRIGGER invalid_trig INSTEAD OF INSERT ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ CREATE TRIGGER invalid_trig INSTEAD OF DELETE ON main_table
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+
+ -- Don't support WHEN clauses with INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Don't support column-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE OF a ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Don't support statement-level INSTEAD OF triggers
+ CREATE TRIGGER invalid_trig INSTEAD OF UPDATE ON main_view
+ EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ -- Valid INSTEAD OF triggers
+ CREATE TRIGGER instead_of_insert_trig INSTEAD OF INSERT ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_ins');
+
+ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
+
+ CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
+ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+
+ -- Valid BEFORE statement VIEW triggers
+ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
+
+ CREATE TRIGGER before_upd_stmt_trig BEFORE UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_upd_stmt');
+
+ CREATE TRIGGER before_del_stmt_trig BEFORE DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_del_stmt');
+
+ -- Valid AFTER statement VIEW triggers
+ CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_ins_stmt');
+
+ CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_upd_stmt');
+
+ CREATE TRIGGER after_del_stmt_trig AFTER DELETE ON main_view
+ FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('after_view_del_stmt');
+
+ \set QUIET false
+
+ -- Insert into view using trigger
+ INSERT INTO main_view VALUES (20, 30);
+ INSERT INTO main_view VALUES (21, 31) RETURNING a, b;
+
+ -- Table trigger will prevent updates
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+
+ -- Remove table trigger to allow updates
+ DROP TRIGGER before_upd_a_row_trig ON main_table;
+ UPDATE main_view SET b = 31 WHERE a = 20;
+ UPDATE main_view SET b = 32 WHERE a = 21 AND b = 31 RETURNING a, b;
+
+ -- Before and after stmt triggers should fire even when no rows are affected
+ UPDATE main_view SET b = 0 WHERE false;
+
+ -- Delete from view using trigger
+ DELETE FROM main_view WHERE a IN (20,21);
+ DELETE FROM main_view WHERE a = 31 RETURNING a, b;
+
+ \set QUIET true
+
+ -- Describe view should list triggers
+ \d main_view
+
+ -- Test dropping view triggers
+ DROP TRIGGER instead_of_insert_trig ON main_view;
+ DROP TRIGGER instead_of_delete_trig ON main_view;
+ \d+ main_view
+ DROP VIEW main_view;
+
+ --
+ -- Test triggers on a join view
+ --
+ CREATE TABLE country_table (
+ country_id serial primary key,
+ country_name text unique not null,
+ continent text not null
+ );
+
+ INSERT INTO country_table (country_name, continent)
+ VALUES ('Japan', 'Asia'),
+ ('UK', 'Europe'),
+ ('USA', 'North America')
+ RETURNING *;
+
+ CREATE TABLE city_table (
+ city_id serial primary key,
+ city_name text not null,
+ population bigint,
+ country_id int references country_table
+ );
+
+ CREATE VIEW city_view AS
+ SELECT city_id, city_name, population, country_name, continent
+ FROM city_table ci
+ LEFT JOIN country_table co ON co.country_id = ci.country_id;
+
+ CREATE FUNCTION city_insert() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS NOT NULL then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+ else
+ NEW.continent := NULL;
+ end if;
+
+ if NEW.city_id IS NOT NULL then
+ INSERT INTO city_table
+ VALUES(NEW.city_id, NEW.city_name, NEW.population, ctry_id);
+ else
+ INSERT INTO city_table(city_name, population, country_id)
+ VALUES(NEW.city_name, NEW.population, ctry_id)
+ RETURNING city_id INTO NEW.city_id;
+ end if;
+
+ RETURN NEW;
+ end;
+ $$;
+
+ CREATE TRIGGER city_insert_trig INSTEAD OF INSERT ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_insert();
+
+ CREATE FUNCTION city_delete() RETURNS trigger LANGUAGE plpgsql AS $$
+ begin
+ DELETE FROM city_table WHERE city_id = OLD.city_id;
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN OLD;
+ end;
+ $$;
+
+ CREATE TRIGGER city_delete_trig INSTEAD OF DELETE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_delete();
+
+ CREATE FUNCTION city_update() RETURNS trigger LANGUAGE plpgsql AS $$
+ declare
+ ctry_id int;
+ begin
+ if NEW.country_name IS DISTINCT FROM OLD.country_name then
+ SELECT country_id, continent INTO ctry_id, NEW.continent
+ FROM country_table WHERE country_name = NEW.country_name;
+ if NOT FOUND then
+ raise exception 'No such country: "%"', NEW.country_name;
+ end if;
+
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population,
+ country_id = ctry_id
+ WHERE city_id = OLD.city_id;
+ else
+ UPDATE city_table SET city_name = NEW.city_name,
+ population = NEW.population
+ WHERE city_id = OLD.city_id;
+ NEW.continent := OLD.continent;
+ end if;
+
+ if NOT FOUND then RETURN NULL; end if;
+ RETURN NEW;
+ end;
+ $$;
+
+ CREATE TRIGGER city_update_trig INSTEAD OF UPDATE ON city_view
+ FOR EACH ROW EXECUTE PROCEDURE city_update();
+
+ -- INSERT .. RETURNING
+ \set QUIET false
+
+ INSERT INTO city_view(city_name) VALUES('Tokyo') RETURNING *;
+ INSERT INTO city_view(city_name, population) VALUES('London', 7556900) RETURNING *;
+ INSERT INTO city_view(city_name, country_name) VALUES('Washington DC', 'USA') RETURNING *;
+ INSERT INTO city_view(city_id, city_name) VALUES(123456, 'New York') RETURNING *;
+ INSERT INTO city_view VALUES(234567, 'Birmingham', 1016800, 'UK', 'EU') RETURNING *;
+
+ -- UPDATE .. RETURNING
+ UPDATE city_view SET country_name = 'Japon' WHERE city_name = 'Tokyo'; -- error
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Takyo'; -- no match
+ UPDATE city_view SET country_name = 'Japan' WHERE city_name = 'Tokyo' RETURNING *; -- OK
+
+ UPDATE city_view SET population = 13010279 WHERE city_name = 'Tokyo' RETURNING *;
+ UPDATE city_view SET country_name = 'UK' WHERE city_name = 'New York' RETURNING *;
+ UPDATE city_view SET country_name = 'USA', population = 8391881 WHERE city_name = 'New York' RETURNING *;
+ UPDATE city_view SET continent = 'EU' WHERE continent = 'Europe' RETURNING *;
+ UPDATE city_view v1 SET country_name = v2.country_name FROM city_view v2
+ WHERE v2.city_name = 'Birmingham' AND v1.city_name = 'London' RETURNING *;
+
+ -- DELETE .. RETURNING
+ DELETE FROM city_view WHERE city_name = 'Birmingham' RETURNING *;
+
+ \set QUIET true
+
+ -- read-only view with WHERE clause
+ CREATE VIEW european_city_view AS
+ SELECT * FROM city_view WHERE continent = 'Europe';
+ SELECT count(*) FROM european_city_view;
+
+ CREATE FUNCTION no_op_trig_fn() RETURNS trigger LANGUAGE plpgsql
+ AS 'begin RETURN NULL; end';
+
+ CREATE TRIGGER no_op_trig INSTEAD OF INSERT OR UPDATE OR DELETE
+ ON european_city_view FOR EACH ROW EXECUTE PROCEDURE no_op_trig_fn();
+
+ \set QUIET false
+
+ INSERT INTO european_city_view VALUES (0, 'x', 10000, 'y', 'z');
+ UPDATE european_city_view SET population = 10000;
+ DELETE FROM european_city_view;
+
+ \set QUIET true
+
+ -- rules bypassing no-op triggers
+ CREATE RULE european_city_insert_rule AS ON INSERT TO european_city_view
+ DO INSTEAD INSERT INTO city_view
+ VALUES (NEW.city_id, NEW.city_name, NEW.population, NEW.country_name, NEW.continent)
+ RETURNING *;
+
+ CREATE RULE european_city_update_rule AS ON UPDATE TO european_city_view
+ DO INSTEAD UPDATE city_view SET
+ city_name = NEW.city_name,
+ population = NEW.population,
+ country_name = NEW.country_name
+ WHERE city_id = OLD.city_id
+ RETURNING NEW.*;
+
+ CREATE RULE european_city_delete_rule AS ON DELETE TO european_city_view
+ DO INSTEAD DELETE FROM city_view WHERE city_id = OLD.city_id RETURNING *;
+
+ -- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are
+ \set QUIET false
+
+ INSERT INTO european_city_view(city_name, country_name)
+ VALUES ('Cambridge', 'USA') RETURNING *;
+ UPDATE european_city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge';
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge';
+
+ -- UPDATE and DELETE via rule and trigger
+ UPDATE city_view SET country_name = 'UK'
+ WHERE city_name = 'Cambridge' RETURNING *;
+ UPDATE european_city_view SET population = 122800
+ WHERE city_name = 'Cambridge' RETURNING *;
+ DELETE FROM european_city_view WHERE city_name = 'Cambridge' RETURNING *;
+
+ -- join UPDATE test
+ UPDATE city_view v SET population = 599657
+ FROM city_table ci, country_table co
+ WHERE ci.city_name = 'Washington DC' and co.country_name = 'USA'
+ AND v.city_id = ci.city_id AND v.country_name = co.country_name
+ RETURNING co.country_id, v.country_name,
+ v.city_id, v.city_name, v.population;
+
+ \set QUIET true
+
+ SELECT * FROM city_view;
+
+ DROP TABLE city_table CASCADE;
+ DROP TABLE country_table;