mirror of
https://github.com/postgres/postgres.git
synced 2025-05-28 05:21:27 +03:00
When an UPDATE trigger referencing a new table and a DELETE trigger referencing an old table are both present, MakeTransitionCaptureState() returns an inconsistent result for UPDATE commands in its set of flags and tuplestores holding the TransitionCaptureState for transition tables. As proved by the test added here, this issue causes a crash in v14 and earlier versions (down to 11, actually, older versions do not support triggers on partitioned tables) during cross-partition updates on a partitioned table. v15 and newer versions are safe thanks to 7103ebb7aae8. This commit fixes the function so that it returns a consistent state by using portions of the changes made in commit 7103ebb7aae8 for v13 and v14. v15 and newer versions are slightly tweaked to match with the older versions, mainly for consistency across branches. Author: Kyotaro Horiguchi Discussion: https://postgr.es/m/20250207.150238.968446820828052276.horikyota.ntt@gmail.com Backpatch-through: 13
3780 lines
164 KiB
Plaintext
3780 lines
164 KiB
Plaintext
--
|
|
-- TRIGGERS
|
|
--
|
|
-- directory paths and dlsuffix are passed to us in environment variables
|
|
\getenv libdir PG_LIBDIR
|
|
\getenv dlsuffix PG_DLSUFFIX
|
|
\set autoinclib :libdir '/autoinc' :dlsuffix
|
|
\set refintlib :libdir '/refint' :dlsuffix
|
|
\set regresslib :libdir '/regress' :dlsuffix
|
|
CREATE FUNCTION autoinc ()
|
|
RETURNS trigger
|
|
AS :'autoinclib'
|
|
LANGUAGE C;
|
|
CREATE FUNCTION check_primary_key ()
|
|
RETURNS trigger
|
|
AS :'refintlib'
|
|
LANGUAGE C;
|
|
CREATE FUNCTION check_foreign_key ()
|
|
RETURNS trigger
|
|
AS :'refintlib'
|
|
LANGUAGE C;
|
|
CREATE FUNCTION trigger_return_old ()
|
|
RETURNS trigger
|
|
AS :'regresslib'
|
|
LANGUAGE C;
|
|
CREATE FUNCTION set_ttdummy (int4)
|
|
RETURNS int4
|
|
AS :'regresslib'
|
|
LANGUAGE C STRICT;
|
|
create table pkeys (pkey1 int4 not null, pkey2 text not null);
|
|
create table fkeys (fkey1 int4, fkey2 text, fkey3 int);
|
|
create table fkeys2 (fkey21 int4, fkey22 text, pkey23 int not null);
|
|
create index fkeys_i on fkeys (fkey1, fkey2);
|
|
create index fkeys2_i on fkeys2 (fkey21, fkey22);
|
|
create index fkeys2p_i on fkeys2 (pkey23);
|
|
insert into pkeys values (10, '1');
|
|
insert into pkeys values (20, '2');
|
|
insert into pkeys values (30, '3');
|
|
insert into pkeys values (40, '4');
|
|
insert into pkeys values (50, '5');
|
|
insert into pkeys values (60, '6');
|
|
create unique index pkeys_i on pkeys (pkey1, pkey2);
|
|
--
|
|
-- For fkeys:
|
|
-- (fkey1, fkey2) --> pkeys (pkey1, pkey2)
|
|
-- (fkey3) --> fkeys2 (pkey23)
|
|
--
|
|
create trigger check_fkeys_pkey_exist
|
|
before insert or update on fkeys
|
|
for each row
|
|
execute function
|
|
check_primary_key ('fkey1', 'fkey2', 'pkeys', 'pkey1', 'pkey2');
|
|
create trigger check_fkeys_pkey2_exist
|
|
before insert or update on fkeys
|
|
for each row
|
|
execute function check_primary_key ('fkey3', 'fkeys2', 'pkey23');
|
|
--
|
|
-- For fkeys2:
|
|
-- (fkey21, fkey22) --> pkeys (pkey1, pkey2)
|
|
--
|
|
create trigger check_fkeys2_pkey_exist
|
|
before insert or update on fkeys2
|
|
for each row
|
|
execute procedure
|
|
check_primary_key ('fkey21', 'fkey22', 'pkeys', 'pkey1', 'pkey2');
|
|
-- Test comments
|
|
COMMENT ON TRIGGER check_fkeys2_pkey_bad ON fkeys2 IS 'wrong';
|
|
ERROR: trigger "check_fkeys2_pkey_bad" for table "fkeys2" does not exist
|
|
COMMENT ON TRIGGER check_fkeys2_pkey_exist ON fkeys2 IS 'right';
|
|
COMMENT ON TRIGGER check_fkeys2_pkey_exist ON fkeys2 IS NULL;
|
|
--
|
|
-- For pkeys:
|
|
-- ON DELETE/UPDATE (pkey1, pkey2) CASCADE:
|
|
-- fkeys (fkey1, fkey2) and fkeys2 (fkey21, fkey22)
|
|
--
|
|
create trigger check_pkeys_fkey_cascade
|
|
before delete or update on pkeys
|
|
for each row
|
|
execute procedure
|
|
check_foreign_key (2, 'cascade', 'pkey1', 'pkey2',
|
|
'fkeys', 'fkey1', 'fkey2', 'fkeys2', 'fkey21', 'fkey22');
|
|
--
|
|
-- For fkeys2:
|
|
-- ON DELETE/UPDATE (pkey23) RESTRICT:
|
|
-- fkeys (fkey3)
|
|
--
|
|
create trigger check_fkeys2_fkey_restrict
|
|
before delete or update on fkeys2
|
|
for each row
|
|
execute procedure check_foreign_key (1, 'restrict', 'pkey23', 'fkeys', 'fkey3');
|
|
insert into fkeys2 values (10, '1', 1);
|
|
insert into fkeys2 values (30, '3', 2);
|
|
insert into fkeys2 values (40, '4', 5);
|
|
insert into fkeys2 values (50, '5', 3);
|
|
-- no key in pkeys
|
|
insert into fkeys2 values (70, '5', 3);
|
|
ERROR: tuple references non-existent key
|
|
DETAIL: Trigger "check_fkeys2_pkey_exist" found tuple referencing non-existent key in "pkeys".
|
|
insert into fkeys values (10, '1', 2);
|
|
insert into fkeys values (30, '3', 3);
|
|
insert into fkeys values (40, '4', 2);
|
|
insert into fkeys values (50, '5', 2);
|
|
-- no key in pkeys
|
|
insert into fkeys values (70, '5', 1);
|
|
ERROR: tuple references non-existent key
|
|
DETAIL: Trigger "check_fkeys_pkey_exist" found tuple referencing non-existent key in "pkeys".
|
|
-- no key in fkeys2
|
|
insert into fkeys values (60, '6', 4);
|
|
ERROR: tuple references non-existent key
|
|
DETAIL: Trigger "check_fkeys_pkey2_exist" found tuple referencing non-existent key in "fkeys2".
|
|
delete from pkeys where pkey1 = 30 and pkey2 = '3';
|
|
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are deleted
|
|
ERROR: "check_fkeys2_fkey_restrict": tuple is referenced in "fkeys"
|
|
CONTEXT: SQL statement "delete from fkeys2 where fkey21 = $1 and fkey22 = $2 "
|
|
delete from pkeys where pkey1 = 40 and pkey2 = '4';
|
|
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are deleted
|
|
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys2 are deleted
|
|
update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 50 and pkey2 = '5';
|
|
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are deleted
|
|
ERROR: "check_fkeys2_fkey_restrict": tuple is referenced in "fkeys"
|
|
CONTEXT: SQL statement "delete from fkeys2 where fkey21 = $1 and fkey22 = $2 "
|
|
update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 10 and pkey2 = '1';
|
|
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are deleted
|
|
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys2 are deleted
|
|
SELECT trigger_name, event_manipulation, event_object_schema, event_object_table,
|
|
action_order, action_condition, action_orientation, action_timing,
|
|
action_reference_old_table, action_reference_new_table
|
|
FROM information_schema.triggers
|
|
WHERE event_object_table in ('pkeys', 'fkeys', 'fkeys2')
|
|
ORDER BY trigger_name COLLATE "C", 2;
|
|
trigger_name | event_manipulation | event_object_schema | event_object_table | action_order | action_condition | action_orientation | action_timing | action_reference_old_table | action_reference_new_table
|
|
----------------------------+--------------------+---------------------+--------------------+--------------+------------------+--------------------+---------------+----------------------------+----------------------------
|
|
check_fkeys2_fkey_restrict | DELETE | public | fkeys2 | 1 | | ROW | BEFORE | |
|
|
check_fkeys2_fkey_restrict | UPDATE | public | fkeys2 | 1 | | ROW | BEFORE | |
|
|
check_fkeys2_pkey_exist | INSERT | public | fkeys2 | 1 | | ROW | BEFORE | |
|
|
check_fkeys2_pkey_exist | UPDATE | public | fkeys2 | 2 | | ROW | BEFORE | |
|
|
check_fkeys_pkey2_exist | INSERT | public | fkeys | 1 | | ROW | BEFORE | |
|
|
check_fkeys_pkey2_exist | UPDATE | public | fkeys | 1 | | ROW | BEFORE | |
|
|
check_fkeys_pkey_exist | INSERT | public | fkeys | 2 | | ROW | BEFORE | |
|
|
check_fkeys_pkey_exist | UPDATE | public | fkeys | 2 | | ROW | BEFORE | |
|
|
check_pkeys_fkey_cascade | DELETE | public | pkeys | 1 | | ROW | BEFORE | |
|
|
check_pkeys_fkey_cascade | UPDATE | public | pkeys | 1 | | ROW | BEFORE | |
|
|
(10 rows)
|
|
|
|
DROP TABLE pkeys;
|
|
DROP TABLE fkeys;
|
|
DROP TABLE fkeys2;
|
|
-- Check behavior when trigger returns unmodified trigtuple
|
|
create table trigtest (f1 int, f2 text);
|
|
create trigger trigger_return_old
|
|
before insert or delete or update on trigtest
|
|
for each row execute procedure trigger_return_old();
|
|
insert into trigtest values(1, 'foo');
|
|
select * from trigtest;
|
|
f1 | f2
|
|
----+-----
|
|
1 | foo
|
|
(1 row)
|
|
|
|
update trigtest set f2 = f2 || 'bar';
|
|
select * from trigtest;
|
|
f1 | f2
|
|
----+-----
|
|
1 | foo
|
|
(1 row)
|
|
|
|
delete from trigtest;
|
|
select * from trigtest;
|
|
f1 | f2
|
|
----+----
|
|
(0 rows)
|
|
|
|
-- Also check what happens when such a trigger runs before or after others
|
|
create function f1_times_10() returns trigger as
|
|
$$ begin new.f1 := new.f1 * 10; return new; end $$ language plpgsql;
|
|
create trigger trigger_alpha
|
|
before insert or update on trigtest
|
|
for each row execute procedure f1_times_10();
|
|
insert into trigtest values(1, 'foo');
|
|
select * from trigtest;
|
|
f1 | f2
|
|
----+-----
|
|
10 | foo
|
|
(1 row)
|
|
|
|
update trigtest set f2 = f2 || 'bar';
|
|
select * from trigtest;
|
|
f1 | f2
|
|
----+-----
|
|
10 | foo
|
|
(1 row)
|
|
|
|
delete from trigtest;
|
|
select * from trigtest;
|
|
f1 | f2
|
|
----+----
|
|
(0 rows)
|
|
|
|
create trigger trigger_zed
|
|
before insert or update on trigtest
|
|
for each row execute procedure f1_times_10();
|
|
insert into trigtest values(1, 'foo');
|
|
select * from trigtest;
|
|
f1 | f2
|
|
-----+-----
|
|
100 | foo
|
|
(1 row)
|
|
|
|
update trigtest set f2 = f2 || 'bar';
|
|
select * from trigtest;
|
|
f1 | f2
|
|
------+-----
|
|
1000 | foo
|
|
(1 row)
|
|
|
|
delete from trigtest;
|
|
select * from trigtest;
|
|
f1 | f2
|
|
----+----
|
|
(0 rows)
|
|
|
|
drop trigger trigger_alpha on trigtest;
|
|
insert into trigtest values(1, 'foo');
|
|
select * from trigtest;
|
|
f1 | f2
|
|
----+-----
|
|
10 | foo
|
|
(1 row)
|
|
|
|
update trigtest set f2 = f2 || 'bar';
|
|
select * from trigtest;
|
|
f1 | f2
|
|
-----+-----
|
|
100 | foo
|
|
(1 row)
|
|
|
|
delete from trigtest;
|
|
select * from trigtest;
|
|
f1 | f2
|
|
----+----
|
|
(0 rows)
|
|
|
|
drop table trigtest;
|
|
-- Check behavior with an implicit column default, too (bug #16644)
|
|
create table trigtest (
|
|
a integer,
|
|
b bool default true not null,
|
|
c text default 'xyzzy' not null);
|
|
create trigger trigger_return_old
|
|
before insert or delete or update on trigtest
|
|
for each row execute procedure trigger_return_old();
|
|
insert into trigtest values(1);
|
|
select * from trigtest;
|
|
a | b | c
|
|
---+---+-------
|
|
1 | t | xyzzy
|
|
(1 row)
|
|
|
|
alter table trigtest add column d integer default 42 not null;
|
|
select * from trigtest;
|
|
a | b | c | d
|
|
---+---+-------+----
|
|
1 | t | xyzzy | 42
|
|
(1 row)
|
|
|
|
update trigtest set a = 2 where a = 1 returning *;
|
|
a | b | c | d
|
|
---+---+-------+----
|
|
1 | t | xyzzy | 42
|
|
(1 row)
|
|
|
|
select * from trigtest;
|
|
a | b | c | d
|
|
---+---+-------+----
|
|
1 | t | xyzzy | 42
|
|
(1 row)
|
|
|
|
alter table trigtest drop column b;
|
|
select * from trigtest;
|
|
a | c | d
|
|
---+-------+----
|
|
1 | xyzzy | 42
|
|
(1 row)
|
|
|
|
update trigtest set a = 2 where a = 1 returning *;
|
|
a | c | d
|
|
---+-------+----
|
|
1 | xyzzy | 42
|
|
(1 row)
|
|
|
|
select * from trigtest;
|
|
a | c | d
|
|
---+-------+----
|
|
1 | xyzzy | 42
|
|
(1 row)
|
|
|
|
drop table trigtest;
|
|
create sequence ttdummy_seq increment 10 start 0 minvalue 0;
|
|
create table tttest (
|
|
price_id int4,
|
|
price_val int4,
|
|
price_on int4,
|
|
price_off int4 default 999999
|
|
);
|
|
create trigger ttdummy
|
|
before delete or update on tttest
|
|
for each row
|
|
execute procedure
|
|
ttdummy (price_on, price_off);
|
|
create trigger ttserial
|
|
before insert or update on tttest
|
|
for each row
|
|
execute procedure
|
|
autoinc (price_on, ttdummy_seq);
|
|
insert into tttest values (1, 1, null);
|
|
insert into tttest values (2, 2, null);
|
|
insert into tttest values (3, 3, 0);
|
|
select * from tttest;
|
|
price_id | price_val | price_on | price_off
|
|
----------+-----------+----------+-----------
|
|
1 | 1 | 10 | 999999
|
|
2 | 2 | 20 | 999999
|
|
3 | 3 | 30 | 999999
|
|
(3 rows)
|
|
|
|
delete from tttest where price_id = 2;
|
|
select * from tttest;
|
|
price_id | price_val | price_on | price_off
|
|
----------+-----------+----------+-----------
|
|
1 | 1 | 10 | 999999
|
|
3 | 3 | 30 | 999999
|
|
2 | 2 | 20 | 40
|
|
(3 rows)
|
|
|
|
-- what do we see ?
|
|
-- get current prices
|
|
select * from tttest where price_off = 999999;
|
|
price_id | price_val | price_on | price_off
|
|
----------+-----------+----------+-----------
|
|
1 | 1 | 10 | 999999
|
|
3 | 3 | 30 | 999999
|
|
(2 rows)
|
|
|
|
-- change price for price_id == 3
|
|
update tttest set price_val = 30 where price_id = 3;
|
|
select * from tttest;
|
|
price_id | price_val | price_on | price_off
|
|
----------+-----------+----------+-----------
|
|
1 | 1 | 10 | 999999
|
|
2 | 2 | 20 | 40
|
|
3 | 30 | 50 | 999999
|
|
3 | 3 | 30 | 50
|
|
(4 rows)
|
|
|
|
-- now we want to change pric_id in ALL tuples
|
|
-- this gets us not what we need
|
|
update tttest set price_id = 5 where price_id = 3;
|
|
select * from tttest;
|
|
price_id | price_val | price_on | price_off
|
|
----------+-----------+----------+-----------
|
|
1 | 1 | 10 | 999999
|
|
2 | 2 | 20 | 40
|
|
3 | 3 | 30 | 50
|
|
5 | 30 | 60 | 999999
|
|
3 | 30 | 50 | 60
|
|
(5 rows)
|
|
|
|
-- restore data as before last update:
|
|
select set_ttdummy(0);
|
|
set_ttdummy
|
|
-------------
|
|
1
|
|
(1 row)
|
|
|
|
delete from tttest where price_id = 5;
|
|
update tttest set price_off = 999999 where price_val = 30;
|
|
select * from tttest;
|
|
price_id | price_val | price_on | price_off
|
|
----------+-----------+----------+-----------
|
|
1 | 1 | 10 | 999999
|
|
2 | 2 | 20 | 40
|
|
3 | 3 | 30 | 50
|
|
3 | 30 | 50 | 999999
|
|
(4 rows)
|
|
|
|
-- and try change price_id now!
|
|
update tttest set price_id = 5 where price_id = 3;
|
|
select * from tttest;
|
|
price_id | price_val | price_on | price_off
|
|
----------+-----------+----------+-----------
|
|
1 | 1 | 10 | 999999
|
|
2 | 2 | 20 | 40
|
|
5 | 3 | 30 | 50
|
|
5 | 30 | 50 | 999999
|
|
(4 rows)
|
|
|
|
-- isn't it what we need ?
|
|
select set_ttdummy(1);
|
|
set_ttdummy
|
|
-------------
|
|
0
|
|
(1 row)
|
|
|
|
-- we want to correct some "date"
|
|
update tttest set price_on = -1 where price_id = 1;
|
|
ERROR: ttdummy (tttest): you cannot change price_on and/or price_off columns (use set_ttdummy)
|
|
-- but this doesn't work
|
|
-- try in this way
|
|
select set_ttdummy(0);
|
|
set_ttdummy
|
|
-------------
|
|
1
|
|
(1 row)
|
|
|
|
update tttest set price_on = -1 where price_id = 1;
|
|
select * from tttest;
|
|
price_id | price_val | price_on | price_off
|
|
----------+-----------+----------+-----------
|
|
2 | 2 | 20 | 40
|
|
5 | 3 | 30 | 50
|
|
5 | 30 | 50 | 999999
|
|
1 | 1 | -1 | 999999
|
|
(4 rows)
|
|
|
|
-- isn't it what we need ?
|
|
-- get price for price_id == 5 as it was @ "date" 35
|
|
select * from tttest where price_on <= 35 and price_off > 35 and price_id = 5;
|
|
price_id | price_val | price_on | price_off
|
|
----------+-----------+----------+-----------
|
|
5 | 3 | 30 | 50
|
|
(1 row)
|
|
|
|
drop table tttest;
|
|
drop sequence ttdummy_seq;
|
|
--
|
|
-- tests for per-statement triggers
|
|
--
|
|
CREATE TABLE log_table (tstamp timestamp default timeofday()::timestamp);
|
|
CREATE TABLE main_table (a int unique, b int);
|
|
COPY main_table (a,b) FROM stdin;
|
|
CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
|
|
BEGIN
|
|
RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
|
|
RETURN NULL;
|
|
END;';
|
|
CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_table
|
|
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
|
|
CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
|
|
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
|
|
--
|
|
-- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
|
|
-- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
|
|
--
|
|
CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_table
|
|
EXECUTE PROCEDURE trigger_func('after_upd_stmt');
|
|
-- Both insert and update statement level triggers (before and after) should
|
|
-- fire. Doesn't fire UPDATE before trigger, but only because one isn't
|
|
-- defined.
|
|
INSERT INTO main_table (a, b) VALUES (5, 10) ON CONFLICT (a)
|
|
DO UPDATE SET b = EXCLUDED.b;
|
|
NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
|
|
CREATE TRIGGER after_upd_row_trig AFTER UPDATE ON main_table
|
|
FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_row');
|
|
INSERT INTO main_table DEFAULT VALUES;
|
|
NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
|
|
UPDATE main_table SET a = a + 1 WHERE b < 30;
|
|
NOTICE: trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
-- UPDATE that effects zero rows should still call per-statement trigger
|
|
UPDATE main_table SET a = a + 2 WHERE b > 100;
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
-- constraint now unneeded
|
|
ALTER TABLE main_table DROP CONSTRAINT main_table_a_key;
|
|
-- COPY should fire per-row and per-statement INSERT triggers
|
|
COPY main_table (a, b) FROM stdin;
|
|
NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
|
|
SELECT * FROM main_table ORDER BY a, b;
|
|
a | b
|
|
----+----
|
|
6 | 10
|
|
21 | 20
|
|
30 | 40
|
|
31 | 10
|
|
50 | 35
|
|
50 | 60
|
|
81 | 15
|
|
|
|
|
(8 rows)
|
|
|
|
--
|
|
-- test triggers with WHEN clause
|
|
--
|
|
CREATE TRIGGER modified_a BEFORE UPDATE OF a ON main_table
|
|
FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE trigger_func('modified_a');
|
|
CREATE TRIGGER modified_any BEFORE UPDATE OF a ON main_table
|
|
FOR EACH ROW WHEN (OLD.* IS DISTINCT FROM NEW.*) EXECUTE PROCEDURE trigger_func('modified_any');
|
|
CREATE TRIGGER insert_a AFTER INSERT ON main_table
|
|
FOR EACH ROW WHEN (NEW.a = 123) EXECUTE PROCEDURE trigger_func('insert_a');
|
|
CREATE TRIGGER delete_a AFTER DELETE ON main_table
|
|
FOR EACH ROW WHEN (OLD.a = 123) EXECUTE PROCEDURE trigger_func('delete_a');
|
|
CREATE TRIGGER insert_when BEFORE INSERT ON main_table
|
|
FOR EACH STATEMENT WHEN (true) EXECUTE PROCEDURE trigger_func('insert_when');
|
|
CREATE TRIGGER delete_when AFTER DELETE ON main_table
|
|
FOR EACH STATEMENT WHEN (true) EXECUTE PROCEDURE trigger_func('delete_when');
|
|
SELECT trigger_name, event_manipulation, event_object_schema, event_object_table,
|
|
action_order, action_condition, action_orientation, action_timing,
|
|
action_reference_old_table, action_reference_new_table
|
|
FROM information_schema.triggers
|
|
WHERE event_object_table IN ('main_table')
|
|
ORDER BY trigger_name COLLATE "C", 2;
|
|
trigger_name | event_manipulation | event_object_schema | event_object_table | action_order | action_condition | action_orientation | action_timing | action_reference_old_table | action_reference_new_table
|
|
----------------------+--------------------+---------------------+--------------------+--------------+--------------------------------+--------------------+---------------+----------------------------+----------------------------
|
|
after_ins_stmt_trig | INSERT | public | main_table | 1 | | STATEMENT | AFTER | |
|
|
after_upd_row_trig | UPDATE | public | main_table | 1 | | ROW | AFTER | |
|
|
after_upd_stmt_trig | UPDATE | public | main_table | 1 | | STATEMENT | AFTER | |
|
|
before_ins_stmt_trig | INSERT | public | main_table | 1 | | STATEMENT | BEFORE | |
|
|
delete_a | DELETE | public | main_table | 1 | (old.a = 123) | ROW | AFTER | |
|
|
delete_when | DELETE | public | main_table | 1 | true | STATEMENT | AFTER | |
|
|
insert_a | INSERT | public | main_table | 1 | (new.a = 123) | ROW | AFTER | |
|
|
insert_when | INSERT | public | main_table | 2 | true | STATEMENT | BEFORE | |
|
|
modified_a | UPDATE | public | main_table | 1 | (old.a <> new.a) | ROW | BEFORE | |
|
|
modified_any | UPDATE | public | main_table | 2 | (old.* IS DISTINCT FROM new.*) | ROW | BEFORE | |
|
|
(10 rows)
|
|
|
|
INSERT INTO main_table (a) VALUES (123), (456);
|
|
NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger_func(insert_when) called: action = INSERT, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger_func(insert_a) called: action = INSERT, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
|
|
COPY main_table FROM stdin;
|
|
NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger_func(insert_when) called: action = INSERT, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger_func(insert_a) called: action = INSERT, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
|
|
DELETE FROM main_table WHERE a IN (123, 456);
|
|
NOTICE: trigger_func(delete_a) called: action = DELETE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(delete_a) called: action = DELETE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(delete_when) called: action = DELETE, when = AFTER, level = STATEMENT
|
|
UPDATE main_table SET a = 50, b = 60;
|
|
NOTICE: trigger_func(modified_any) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(modified_any) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(modified_a) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(modified_a) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(modified_a) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(modified_a) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(modified_a) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
SELECT * FROM main_table ORDER BY a, b;
|
|
a | b
|
|
----+----
|
|
6 | 10
|
|
21 | 20
|
|
30 | 40
|
|
31 | 10
|
|
50 | 35
|
|
50 | 60
|
|
81 | 15
|
|
|
|
|
(8 rows)
|
|
|
|
SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_a';
|
|
pg_get_triggerdef
|
|
-------------------------------------------------------------------------------------------------------------------------------------------
|
|
CREATE TRIGGER modified_a BEFORE UPDATE OF a ON main_table FOR EACH ROW WHEN (old.a <> new.a) EXECUTE FUNCTION trigger_func('modified_a')
|
|
(1 row)
|
|
|
|
SELECT pg_get_triggerdef(oid, false) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_a';
|
|
pg_get_triggerdef
|
|
----------------------------------------------------------------------------------------------------------------------------------------------------
|
|
CREATE TRIGGER modified_a BEFORE UPDATE OF a ON public.main_table FOR EACH ROW WHEN ((old.a <> new.a)) EXECUTE FUNCTION trigger_func('modified_a')
|
|
(1 row)
|
|
|
|
SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_any';
|
|
pg_get_triggerdef
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
CREATE TRIGGER modified_any BEFORE UPDATE OF a ON main_table FOR EACH ROW WHEN (old.* IS DISTINCT FROM new.*) EXECUTE FUNCTION trigger_func('modified_any')
|
|
(1 row)
|
|
|
|
-- Test RENAME TRIGGER
|
|
ALTER TRIGGER modified_a ON main_table RENAME TO modified_modified_a;
|
|
SELECT count(*) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_a';
|
|
count
|
|
-------
|
|
0
|
|
(1 row)
|
|
|
|
SELECT count(*) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_modified_a';
|
|
count
|
|
-------
|
|
1
|
|
(1 row)
|
|
|
|
DROP TRIGGER modified_modified_a ON main_table;
|
|
DROP TRIGGER modified_any ON main_table;
|
|
DROP TRIGGER insert_a ON main_table;
|
|
DROP TRIGGER delete_a ON main_table;
|
|
DROP TRIGGER insert_when ON main_table;
|
|
DROP TRIGGER delete_when ON main_table;
|
|
-- Test WHEN condition accessing system columns.
|
|
create table table_with_oids(a int);
|
|
insert into table_with_oids values (1);
|
|
create trigger oid_unchanged_trig after update on table_with_oids
|
|
for each row
|
|
when (new.tableoid = old.tableoid AND new.tableoid <> 0)
|
|
execute procedure trigger_func('after_upd_oid_unchanged');
|
|
update table_with_oids set a = a + 1;
|
|
NOTICE: trigger_func(after_upd_oid_unchanged) called: action = UPDATE, when = AFTER, level = ROW
|
|
drop table table_with_oids;
|
|
-- Test column-level triggers
|
|
DROP TRIGGER after_upd_row_trig ON main_table;
|
|
CREATE TRIGGER before_upd_a_row_trig BEFORE UPDATE OF a ON main_table
|
|
FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_a_row');
|
|
CREATE TRIGGER after_upd_b_row_trig AFTER UPDATE OF b ON main_table
|
|
FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_b_row');
|
|
CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table
|
|
FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_a_b_row');
|
|
CREATE TRIGGER before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table
|
|
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_upd_a_stmt');
|
|
CREATE TRIGGER after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table
|
|
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_upd_b_stmt');
|
|
SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig';
|
|
pg_get_triggerdef
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------
|
|
CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b ON public.main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row')
|
|
(1 row)
|
|
|
|
UPDATE main_table SET a = 50;
|
|
NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
UPDATE main_table SET b = 10;
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
--
|
|
-- Test case for bug with BEFORE trigger followed by AFTER trigger with WHEN
|
|
--
|
|
CREATE TABLE some_t (some_col boolean NOT NULL);
|
|
CREATE FUNCTION dummy_update_func() RETURNS trigger AS $$
|
|
BEGIN
|
|
RAISE NOTICE 'dummy_update_func(%) called: action = %, old = %, new = %',
|
|
TG_ARGV[0], TG_OP, OLD, NEW;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
CREATE TRIGGER some_trig_before BEFORE UPDATE ON some_t FOR EACH ROW
|
|
EXECUTE PROCEDURE dummy_update_func('before');
|
|
CREATE TRIGGER some_trig_aftera AFTER UPDATE ON some_t FOR EACH ROW
|
|
WHEN (NOT OLD.some_col AND NEW.some_col)
|
|
EXECUTE PROCEDURE dummy_update_func('aftera');
|
|
CREATE TRIGGER some_trig_afterb AFTER UPDATE ON some_t FOR EACH ROW
|
|
WHEN (NOT NEW.some_col)
|
|
EXECUTE PROCEDURE dummy_update_func('afterb');
|
|
INSERT INTO some_t VALUES (TRUE);
|
|
UPDATE some_t SET some_col = TRUE;
|
|
NOTICE: dummy_update_func(before) called: action = UPDATE, old = (t), new = (t)
|
|
UPDATE some_t SET some_col = FALSE;
|
|
NOTICE: dummy_update_func(before) called: action = UPDATE, old = (t), new = (f)
|
|
NOTICE: dummy_update_func(afterb) called: action = UPDATE, old = (t), new = (f)
|
|
UPDATE some_t SET some_col = TRUE;
|
|
NOTICE: dummy_update_func(before) called: action = UPDATE, old = (f), new = (t)
|
|
NOTICE: dummy_update_func(aftera) called: action = UPDATE, old = (f), new = (t)
|
|
DROP TABLE some_t;
|
|
-- bogus cases
|
|
CREATE TRIGGER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_table
|
|
FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_and_col');
|
|
ERROR: duplicate trigger events specified at or near "ON"
|
|
LINE 1: ...ER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_ta...
|
|
^
|
|
CREATE TRIGGER error_upd_a_a BEFORE UPDATE OF a, a ON main_table
|
|
FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_a_a');
|
|
ERROR: column "a" specified more than once
|
|
CREATE TRIGGER error_ins_a BEFORE INSERT OF a ON main_table
|
|
FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_ins_a');
|
|
ERROR: syntax error at or near "OF"
|
|
LINE 1: CREATE TRIGGER error_ins_a BEFORE INSERT OF a ON main_table
|
|
^
|
|
CREATE TRIGGER error_ins_when BEFORE INSERT OR UPDATE ON main_table
|
|
FOR EACH ROW WHEN (OLD.a <> NEW.a)
|
|
EXECUTE PROCEDURE trigger_func('error_ins_old');
|
|
ERROR: INSERT trigger's WHEN condition cannot reference OLD values
|
|
LINE 2: FOR EACH ROW WHEN (OLD.a <> NEW.a)
|
|
^
|
|
CREATE TRIGGER error_del_when BEFORE DELETE OR UPDATE ON main_table
|
|
FOR EACH ROW WHEN (OLD.a <> NEW.a)
|
|
EXECUTE PROCEDURE trigger_func('error_del_new');
|
|
ERROR: DELETE trigger's WHEN condition cannot reference NEW values
|
|
LINE 2: FOR EACH ROW WHEN (OLD.a <> NEW.a)
|
|
^
|
|
CREATE TRIGGER error_del_when BEFORE INSERT OR UPDATE ON main_table
|
|
FOR EACH ROW WHEN (NEW.tableoid <> 0)
|
|
EXECUTE PROCEDURE trigger_func('error_when_sys_column');
|
|
ERROR: BEFORE trigger's WHEN condition cannot reference NEW system columns
|
|
LINE 2: FOR EACH ROW WHEN (NEW.tableoid <> 0)
|
|
^
|
|
CREATE TRIGGER error_stmt_when BEFORE UPDATE OF a ON main_table
|
|
FOR EACH STATEMENT WHEN (OLD.* IS DISTINCT FROM NEW.*)
|
|
EXECUTE PROCEDURE trigger_func('error_stmt_when');
|
|
ERROR: statement trigger's WHEN condition cannot reference column values
|
|
LINE 2: FOR EACH STATEMENT WHEN (OLD.* IS DISTINCT FROM NEW.*)
|
|
^
|
|
-- check dependency restrictions
|
|
ALTER TABLE main_table DROP COLUMN b;
|
|
ERROR: cannot drop column b of table main_table because other objects depend on it
|
|
DETAIL: trigger after_upd_b_row_trig on table main_table depends on column b of table main_table
|
|
trigger after_upd_a_b_row_trig on table main_table depends on column b of table main_table
|
|
trigger after_upd_b_stmt_trig on table main_table depends on column b of table main_table
|
|
HINT: Use DROP ... CASCADE to drop the dependent objects too.
|
|
-- this should succeed, but we'll roll it back to keep the triggers around
|
|
begin;
|
|
DROP TRIGGER after_upd_a_b_row_trig ON main_table;
|
|
DROP TRIGGER after_upd_b_row_trig ON main_table;
|
|
DROP TRIGGER after_upd_b_stmt_trig ON main_table;
|
|
ALTER TABLE main_table DROP COLUMN b;
|
|
rollback;
|
|
-- Test enable/disable triggers
|
|
create table trigtest (i serial primary key);
|
|
-- test that disabling RI triggers works
|
|
create table trigtest2 (i int references trigtest(i) on delete cascade);
|
|
create function trigtest() returns trigger as $$
|
|
begin
|
|
raise notice '% % % %', TG_TABLE_NAME, TG_OP, TG_WHEN, TG_LEVEL;
|
|
return new;
|
|
end;$$ language plpgsql;
|
|
create trigger trigtest_b_row_tg before insert or update or delete on trigtest
|
|
for each row execute procedure trigtest();
|
|
create trigger trigtest_a_row_tg after insert or update or delete on trigtest
|
|
for each row execute procedure trigtest();
|
|
create trigger trigtest_b_stmt_tg before insert or update or delete on trigtest
|
|
for each statement execute procedure trigtest();
|
|
create trigger trigtest_a_stmt_tg after insert or update or delete on trigtest
|
|
for each statement execute procedure trigtest();
|
|
insert into trigtest default values;
|
|
NOTICE: trigtest INSERT BEFORE STATEMENT
|
|
NOTICE: trigtest INSERT BEFORE ROW
|
|
NOTICE: trigtest INSERT AFTER ROW
|
|
NOTICE: trigtest INSERT AFTER STATEMENT
|
|
alter table trigtest disable trigger trigtest_b_row_tg;
|
|
insert into trigtest default values;
|
|
NOTICE: trigtest INSERT BEFORE STATEMENT
|
|
NOTICE: trigtest INSERT AFTER ROW
|
|
NOTICE: trigtest INSERT AFTER STATEMENT
|
|
alter table trigtest disable trigger user;
|
|
insert into trigtest default values;
|
|
alter table trigtest enable trigger trigtest_a_stmt_tg;
|
|
insert into trigtest default values;
|
|
NOTICE: trigtest INSERT AFTER STATEMENT
|
|
set session_replication_role = replica;
|
|
insert into trigtest default values; -- does not trigger
|
|
alter table trigtest enable always trigger trigtest_a_stmt_tg;
|
|
insert into trigtest default values; -- now it does
|
|
NOTICE: trigtest INSERT AFTER STATEMENT
|
|
reset session_replication_role;
|
|
insert into trigtest2 values(1);
|
|
insert into trigtest2 values(2);
|
|
delete from trigtest where i=2;
|
|
NOTICE: trigtest DELETE AFTER STATEMENT
|
|
select * from trigtest2;
|
|
i
|
|
---
|
|
1
|
|
(1 row)
|
|
|
|
alter table trigtest disable trigger all;
|
|
delete from trigtest where i=1;
|
|
select * from trigtest2;
|
|
i
|
|
---
|
|
1
|
|
(1 row)
|
|
|
|
-- ensure we still insert, even when all triggers are disabled
|
|
insert into trigtest default values;
|
|
select * from trigtest;
|
|
i
|
|
---
|
|
3
|
|
4
|
|
5
|
|
6
|
|
7
|
|
(5 rows)
|
|
|
|
drop table trigtest2;
|
|
drop table trigtest;
|
|
-- dump trigger data
|
|
CREATE TABLE trigger_test (
|
|
i int,
|
|
v varchar
|
|
);
|
|
CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
|
|
declare
|
|
|
|
argstr text;
|
|
relid text;
|
|
|
|
begin
|
|
|
|
relid := TG_relid::regclass;
|
|
|
|
-- plpgsql can't discover its trigger data in a hash like perl and python
|
|
-- can, or by a sort of reflection like tcl can,
|
|
-- so we have to hard code the names.
|
|
raise NOTICE 'TG_NAME: %', TG_name;
|
|
raise NOTICE 'TG_WHEN: %', TG_when;
|
|
raise NOTICE 'TG_LEVEL: %', TG_level;
|
|
raise NOTICE 'TG_OP: %', TG_op;
|
|
raise NOTICE 'TG_RELID::regclass: %', relid;
|
|
raise NOTICE 'TG_RELNAME: %', TG_relname;
|
|
raise NOTICE 'TG_TABLE_NAME: %', TG_table_name;
|
|
raise NOTICE 'TG_TABLE_SCHEMA: %', TG_table_schema;
|
|
raise NOTICE 'TG_NARGS: %', TG_nargs;
|
|
|
|
argstr := '[';
|
|
for i in 0 .. TG_nargs - 1 loop
|
|
if i > 0 then
|
|
argstr := argstr || ', ';
|
|
end if;
|
|
argstr := argstr || TG_argv[i];
|
|
end loop;
|
|
argstr := argstr || ']';
|
|
raise NOTICE 'TG_ARGV: %', argstr;
|
|
|
|
if TG_OP != 'INSERT' then
|
|
raise NOTICE 'OLD: %', OLD;
|
|
end if;
|
|
|
|
if TG_OP != 'DELETE' then
|
|
raise NOTICE 'NEW: %', NEW;
|
|
end if;
|
|
|
|
if TG_OP = 'DELETE' then
|
|
return OLD;
|
|
else
|
|
return NEW;
|
|
end if;
|
|
|
|
end;
|
|
$$;
|
|
CREATE TRIGGER show_trigger_data_trig
|
|
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
|
|
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
|
|
insert into trigger_test values(1,'insert');
|
|
NOTICE: TG_NAME: show_trigger_data_trig
|
|
NOTICE: TG_WHEN: BEFORE
|
|
NOTICE: TG_LEVEL: ROW
|
|
NOTICE: TG_OP: INSERT
|
|
NOTICE: TG_RELID::regclass: trigger_test
|
|
NOTICE: TG_RELNAME: trigger_test
|
|
NOTICE: TG_TABLE_NAME: trigger_test
|
|
NOTICE: TG_TABLE_SCHEMA: public
|
|
NOTICE: TG_NARGS: 2
|
|
NOTICE: TG_ARGV: [23, skidoo]
|
|
NOTICE: NEW: (1,insert)
|
|
update trigger_test set v = 'update' where i = 1;
|
|
NOTICE: TG_NAME: show_trigger_data_trig
|
|
NOTICE: TG_WHEN: BEFORE
|
|
NOTICE: TG_LEVEL: ROW
|
|
NOTICE: TG_OP: UPDATE
|
|
NOTICE: TG_RELID::regclass: trigger_test
|
|
NOTICE: TG_RELNAME: trigger_test
|
|
NOTICE: TG_TABLE_NAME: trigger_test
|
|
NOTICE: TG_TABLE_SCHEMA: public
|
|
NOTICE: TG_NARGS: 2
|
|
NOTICE: TG_ARGV: [23, skidoo]
|
|
NOTICE: OLD: (1,insert)
|
|
NOTICE: NEW: (1,update)
|
|
delete from trigger_test;
|
|
NOTICE: TG_NAME: show_trigger_data_trig
|
|
NOTICE: TG_WHEN: BEFORE
|
|
NOTICE: TG_LEVEL: ROW
|
|
NOTICE: TG_OP: DELETE
|
|
NOTICE: TG_RELID::regclass: trigger_test
|
|
NOTICE: TG_RELNAME: trigger_test
|
|
NOTICE: TG_TABLE_NAME: trigger_test
|
|
NOTICE: TG_TABLE_SCHEMA: public
|
|
NOTICE: TG_NARGS: 2
|
|
NOTICE: TG_ARGV: [23, skidoo]
|
|
NOTICE: OLD: (1,update)
|
|
DROP TRIGGER show_trigger_data_trig on trigger_test;
|
|
DROP FUNCTION trigger_data();
|
|
DROP TABLE trigger_test;
|
|
--
|
|
-- Test use of row comparisons on OLD/NEW
|
|
--
|
|
CREATE TABLE trigger_test (f1 int, f2 text, f3 text);
|
|
-- this is the obvious (and wrong...) way to compare rows
|
|
CREATE FUNCTION mytrigger() RETURNS trigger LANGUAGE plpgsql as $$
|
|
begin
|
|
if row(old.*) = row(new.*) then
|
|
raise notice 'row % not changed', new.f1;
|
|
else
|
|
raise notice 'row % changed', new.f1;
|
|
end if;
|
|
return new;
|
|
end$$;
|
|
CREATE TRIGGER t
|
|
BEFORE UPDATE ON trigger_test
|
|
FOR EACH ROW EXECUTE PROCEDURE mytrigger();
|
|
INSERT INTO trigger_test VALUES(1, 'foo', 'bar');
|
|
INSERT INTO trigger_test VALUES(2, 'baz', 'quux');
|
|
UPDATE trigger_test SET f3 = 'bar';
|
|
NOTICE: row 1 not changed
|
|
NOTICE: row 2 changed
|
|
UPDATE trigger_test SET f3 = NULL;
|
|
NOTICE: row 1 changed
|
|
NOTICE: row 2 changed
|
|
-- this demonstrates that the above isn't really working as desired:
|
|
UPDATE trigger_test SET f3 = NULL;
|
|
NOTICE: row 1 changed
|
|
NOTICE: row 2 changed
|
|
-- the right way when considering nulls is
|
|
CREATE OR REPLACE FUNCTION mytrigger() RETURNS trigger LANGUAGE plpgsql as $$
|
|
begin
|
|
if row(old.*) is distinct from row(new.*) then
|
|
raise notice 'row % changed', new.f1;
|
|
else
|
|
raise notice 'row % not changed', new.f1;
|
|
end if;
|
|
return new;
|
|
end$$;
|
|
UPDATE trigger_test SET f3 = 'bar';
|
|
NOTICE: row 1 changed
|
|
NOTICE: row 2 changed
|
|
UPDATE trigger_test SET f3 = NULL;
|
|
NOTICE: row 1 changed
|
|
NOTICE: row 2 changed
|
|
UPDATE trigger_test SET f3 = NULL;
|
|
NOTICE: row 1 not changed
|
|
NOTICE: row 2 not changed
|
|
DROP TABLE trigger_test;
|
|
DROP FUNCTION mytrigger();
|
|
-- Test snapshot management in serializable transactions involving triggers
|
|
-- per bug report in 6bc73d4c0910042358k3d1adff3qa36f8df75198ecea@mail.gmail.com
|
|
CREATE FUNCTION serializable_update_trig() RETURNS trigger LANGUAGE plpgsql AS
|
|
$$
|
|
declare
|
|
rec record;
|
|
begin
|
|
new.description = 'updated in trigger';
|
|
return new;
|
|
end;
|
|
$$;
|
|
CREATE TABLE serializable_update_tab (
|
|
id int,
|
|
filler text,
|
|
description text
|
|
);
|
|
CREATE TRIGGER serializable_update_trig BEFORE UPDATE ON serializable_update_tab
|
|
FOR EACH ROW EXECUTE PROCEDURE serializable_update_trig();
|
|
INSERT INTO serializable_update_tab SELECT a, repeat('xyzxz', 100), 'new'
|
|
FROM generate_series(1, 50) a;
|
|
BEGIN;
|
|
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
|
UPDATE serializable_update_tab SET description = 'no no', id = 1 WHERE id = 1;
|
|
COMMIT;
|
|
SELECT description FROM serializable_update_tab WHERE id = 1;
|
|
description
|
|
--------------------
|
|
updated in trigger
|
|
(1 row)
|
|
|
|
DROP TABLE serializable_update_tab;
|
|
-- minimal update trigger
|
|
CREATE TABLE min_updates_test (
|
|
f1 text,
|
|
f2 int,
|
|
f3 int);
|
|
INSERT INTO min_updates_test VALUES ('a',1,2),('b','2',null);
|
|
CREATE TRIGGER z_min_update
|
|
BEFORE UPDATE ON min_updates_test
|
|
FOR EACH ROW EXECUTE PROCEDURE suppress_redundant_updates_trigger();
|
|
\set QUIET false
|
|
UPDATE min_updates_test SET f1 = f1;
|
|
UPDATE 0
|
|
UPDATE min_updates_test SET f2 = f2 + 1;
|
|
UPDATE 2
|
|
UPDATE min_updates_test SET f3 = 2 WHERE f3 is null;
|
|
UPDATE 1
|
|
\set QUIET true
|
|
SELECT * FROM min_updates_test;
|
|
f1 | f2 | f3
|
|
----+----+----
|
|
a | 2 | 2
|
|
b | 3 | 2
|
|
(2 rows)
|
|
|
|
DROP TABLE min_updates_test;
|
|
--
|
|
-- Test triggers on views
|
|
--
|
|
CREATE VIEW main_view AS SELECT a, b FROM main_table;
|
|
-- 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_TABLE_NAME, TG_WHEN, TG_OP, TG_LEVEL, argstr;
|
|
|
|
if TG_LEVEL = 'ROW' 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
|
|
DETAIL: Views cannot have row-level BEFORE or AFTER triggers.
|
|
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
|
|
DETAIL: Views cannot have row-level BEFORE or AFTER triggers.
|
|
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
|
|
DETAIL: Views cannot have row-level BEFORE or AFTER triggers.
|
|
-- 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
|
|
DETAIL: Views cannot have row-level BEFORE or AFTER triggers.
|
|
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
|
|
DETAIL: Views cannot have row-level BEFORE or AFTER triggers.
|
|
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
|
|
DETAIL: Views cannot have row-level BEFORE or AFTER triggers.
|
|
-- 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
|
|
DETAIL: Views cannot have TRUNCATE triggers.
|
|
CREATE TRIGGER invalid_trig AFTER TRUNCATE ON main_view
|
|
EXECUTE PROCEDURE trigger_func('before_tru_row');
|
|
ERROR: "main_view" is a view
|
|
DETAIL: Views cannot have TRUNCATE triggers.
|
|
-- 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
|
|
DETAIL: Tables cannot have INSTEAD OF triggers.
|
|
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
|
|
DETAIL: Tables cannot have INSTEAD OF triggers.
|
|
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
|
|
DETAIL: Tables cannot have INSTEAD OF triggers.
|
|
-- 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: INSTEAD OF triggers cannot have WHEN conditions
|
|
-- 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: INSTEAD OF triggers cannot have column lists
|
|
-- 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: INSTEAD OF triggers must be FOR EACH ROW
|
|
-- 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
|
|
NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = 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
|
|
NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = 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
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = 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
|
|
NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = 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
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = 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
|
|
NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
|
|
NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
|
|
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = 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 | Collation | Nullable | Default
|
|
--------+---------+-----------+----------+---------
|
|
a | integer | | |
|
|
b | integer | | |
|
|
Triggers:
|
|
after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('after_view_del_stmt')
|
|
after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('after_view_ins_stmt')
|
|
after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('after_view_upd_stmt')
|
|
before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('before_view_del_stmt')
|
|
before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('before_view_ins_stmt')
|
|
before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('before_view_upd_stmt')
|
|
instead_of_delete_trig INSTEAD OF DELETE ON main_view FOR EACH ROW EXECUTE FUNCTION view_trigger('instead_of_del')
|
|
instead_of_insert_trig INSTEAD OF INSERT ON main_view FOR EACH ROW EXECUTE FUNCTION view_trigger('instead_of_ins')
|
|
instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE FUNCTION 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 | Collation | Nullable | Default | Storage | Description
|
|
--------+---------+-----------+----------+---------+---------+-------------
|
|
a | integer | | | | plain |
|
|
b | integer | | | | plain |
|
|
View definition:
|
|
SELECT a,
|
|
b
|
|
FROM main_table;
|
|
Triggers:
|
|
after_del_stmt_trig AFTER DELETE ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('after_view_del_stmt')
|
|
after_ins_stmt_trig AFTER INSERT ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('after_view_ins_stmt')
|
|
after_upd_stmt_trig AFTER UPDATE ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('after_view_upd_stmt')
|
|
before_del_stmt_trig BEFORE DELETE ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('before_view_del_stmt')
|
|
before_ins_stmt_trig BEFORE INSERT ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('before_view_ins_stmt')
|
|
before_upd_stmt_trig BEFORE UPDATE ON main_view FOR EACH STATEMENT EXECUTE FUNCTION view_trigger('before_view_upd_stmt')
|
|
instead_of_update_trig INSTEAD OF UPDATE ON main_view FOR EACH ROW EXECUTE FUNCTION 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
|
|
);
|
|
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
|
|
);
|
|
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();
|
|
\set QUIET false
|
|
-- INSERT .. RETURNING
|
|
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"
|
|
CONTEXT: PL/pgSQL function city_update() line 9 at RAISE
|
|
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 *;
|
|
\set QUIET false
|
|
-- INSERT not limited by view's WHERE clause, but UPDATE AND DELETE are
|
|
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;
|
|
-- Test pg_trigger_depth()
|
|
create table depth_a (id int not null primary key);
|
|
create table depth_b (id int not null primary key);
|
|
create table depth_c (id int not null primary key);
|
|
create function depth_a_tf() returns trigger
|
|
language plpgsql as $$
|
|
begin
|
|
raise notice '%: depth = %', tg_name, pg_trigger_depth();
|
|
insert into depth_b values (new.id);
|
|
raise notice '%: depth = %', tg_name, pg_trigger_depth();
|
|
return new;
|
|
end;
|
|
$$;
|
|
create trigger depth_a_tr before insert on depth_a
|
|
for each row execute procedure depth_a_tf();
|
|
create function depth_b_tf() returns trigger
|
|
language plpgsql as $$
|
|
begin
|
|
raise notice '%: depth = %', tg_name, pg_trigger_depth();
|
|
begin
|
|
execute 'insert into depth_c values (' || new.id::text || ')';
|
|
exception
|
|
when sqlstate 'U9999' then
|
|
raise notice 'SQLSTATE = U9999: depth = %', pg_trigger_depth();
|
|
end;
|
|
raise notice '%: depth = %', tg_name, pg_trigger_depth();
|
|
if new.id = 1 then
|
|
execute 'insert into depth_c values (' || new.id::text || ')';
|
|
end if;
|
|
return new;
|
|
end;
|
|
$$;
|
|
create trigger depth_b_tr before insert on depth_b
|
|
for each row execute procedure depth_b_tf();
|
|
create function depth_c_tf() returns trigger
|
|
language plpgsql as $$
|
|
begin
|
|
raise notice '%: depth = %', tg_name, pg_trigger_depth();
|
|
if new.id = 1 then
|
|
raise exception sqlstate 'U9999';
|
|
end if;
|
|
raise notice '%: depth = %', tg_name, pg_trigger_depth();
|
|
return new;
|
|
end;
|
|
$$;
|
|
create trigger depth_c_tr before insert on depth_c
|
|
for each row execute procedure depth_c_tf();
|
|
select pg_trigger_depth();
|
|
pg_trigger_depth
|
|
------------------
|
|
0
|
|
(1 row)
|
|
|
|
insert into depth_a values (1);
|
|
NOTICE: depth_a_tr: depth = 1
|
|
NOTICE: depth_b_tr: depth = 2
|
|
NOTICE: depth_c_tr: depth = 3
|
|
NOTICE: SQLSTATE = U9999: depth = 2
|
|
NOTICE: depth_b_tr: depth = 2
|
|
NOTICE: depth_c_tr: depth = 3
|
|
ERROR: U9999
|
|
CONTEXT: PL/pgSQL function depth_c_tf() line 5 at RAISE
|
|
SQL statement "insert into depth_c values (1)"
|
|
PL/pgSQL function depth_b_tf() line 12 at EXECUTE
|
|
SQL statement "insert into depth_b values (new.id)"
|
|
PL/pgSQL function depth_a_tf() line 4 at SQL statement
|
|
select pg_trigger_depth();
|
|
pg_trigger_depth
|
|
------------------
|
|
0
|
|
(1 row)
|
|
|
|
insert into depth_a values (2);
|
|
NOTICE: depth_a_tr: depth = 1
|
|
NOTICE: depth_b_tr: depth = 2
|
|
NOTICE: depth_c_tr: depth = 3
|
|
NOTICE: depth_c_tr: depth = 3
|
|
NOTICE: depth_b_tr: depth = 2
|
|
NOTICE: depth_a_tr: depth = 1
|
|
select pg_trigger_depth();
|
|
pg_trigger_depth
|
|
------------------
|
|
0
|
|
(1 row)
|
|
|
|
drop table depth_a, depth_b, depth_c;
|
|
drop function depth_a_tf();
|
|
drop function depth_b_tf();
|
|
drop function depth_c_tf();
|
|
--
|
|
-- Test updates to rows during firing of BEFORE ROW triggers.
|
|
-- As of 9.2, such cases should be rejected (see bug #6123).
|
|
--
|
|
create temp table parent (
|
|
aid int not null primary key,
|
|
val1 text,
|
|
val2 text,
|
|
val3 text,
|
|
val4 text,
|
|
bcnt int not null default 0);
|
|
create temp table child (
|
|
bid int not null primary key,
|
|
aid int not null,
|
|
val1 text);
|
|
create function parent_upd_func()
|
|
returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
if old.val1 <> new.val1 then
|
|
new.val2 = new.val1;
|
|
delete from child where child.aid = new.aid and child.val1 = new.val1;
|
|
end if;
|
|
return new;
|
|
end;
|
|
$$;
|
|
create trigger parent_upd_trig before update on parent
|
|
for each row execute procedure parent_upd_func();
|
|
create function parent_del_func()
|
|
returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
delete from child where aid = old.aid;
|
|
return old;
|
|
end;
|
|
$$;
|
|
create trigger parent_del_trig before delete on parent
|
|
for each row execute procedure parent_del_func();
|
|
create function child_ins_func()
|
|
returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
update parent set bcnt = bcnt + 1 where aid = new.aid;
|
|
return new;
|
|
end;
|
|
$$;
|
|
create trigger child_ins_trig after insert on child
|
|
for each row execute procedure child_ins_func();
|
|
create function child_del_func()
|
|
returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
update parent set bcnt = bcnt - 1 where aid = old.aid;
|
|
return old;
|
|
end;
|
|
$$;
|
|
create trigger child_del_trig after delete on child
|
|
for each row execute procedure child_del_func();
|
|
insert into parent values (1, 'a', 'a', 'a', 'a', 0);
|
|
insert into child values (10, 1, 'b');
|
|
select * from parent; select * from child;
|
|
aid | val1 | val2 | val3 | val4 | bcnt
|
|
-----+------+------+------+------+------
|
|
1 | a | a | a | a | 1
|
|
(1 row)
|
|
|
|
bid | aid | val1
|
|
-----+-----+------
|
|
10 | 1 | b
|
|
(1 row)
|
|
|
|
update parent set val1 = 'b' where aid = 1; -- should fail
|
|
ERROR: tuple to be updated was already modified by an operation triggered by the current command
|
|
HINT: Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.
|
|
merge into parent p using (values (1)) as v(id) on p.aid = v.id
|
|
when matched then update set val1 = 'b'; -- should fail
|
|
ERROR: tuple to be updated or deleted was already modified by an operation triggered by the current command
|
|
HINT: Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.
|
|
select * from parent; select * from child;
|
|
aid | val1 | val2 | val3 | val4 | bcnt
|
|
-----+------+------+------+------+------
|
|
1 | a | a | a | a | 1
|
|
(1 row)
|
|
|
|
bid | aid | val1
|
|
-----+-----+------
|
|
10 | 1 | b
|
|
(1 row)
|
|
|
|
delete from parent where aid = 1; -- should fail
|
|
ERROR: tuple to be deleted was already modified by an operation triggered by the current command
|
|
HINT: Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.
|
|
merge into parent p using (values (1)) as v(id) on p.aid = v.id
|
|
when matched then delete; -- should fail
|
|
ERROR: tuple to be updated or deleted was already modified by an operation triggered by the current command
|
|
HINT: Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.
|
|
select * from parent; select * from child;
|
|
aid | val1 | val2 | val3 | val4 | bcnt
|
|
-----+------+------+------+------+------
|
|
1 | a | a | a | a | 1
|
|
(1 row)
|
|
|
|
bid | aid | val1
|
|
-----+-----+------
|
|
10 | 1 | b
|
|
(1 row)
|
|
|
|
-- replace the trigger function with one that restarts the deletion after
|
|
-- having modified a child
|
|
create or replace function parent_del_func()
|
|
returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
delete from child where aid = old.aid;
|
|
if found then
|
|
delete from parent where aid = old.aid;
|
|
return null; -- cancel outer deletion
|
|
end if;
|
|
return old;
|
|
end;
|
|
$$;
|
|
delete from parent where aid = 1;
|
|
select * from parent; select * from child;
|
|
aid | val1 | val2 | val3 | val4 | bcnt
|
|
-----+------+------+------+------+------
|
|
(0 rows)
|
|
|
|
bid | aid | val1
|
|
-----+-----+------
|
|
(0 rows)
|
|
|
|
drop table parent, child;
|
|
drop function parent_upd_func();
|
|
drop function parent_del_func();
|
|
drop function child_ins_func();
|
|
drop function child_del_func();
|
|
-- similar case, but with a self-referencing FK so that parent and child
|
|
-- rows can be affected by a single operation
|
|
create temp table self_ref_trigger (
|
|
id int primary key,
|
|
parent int references self_ref_trigger,
|
|
data text,
|
|
nchildren int not null default 0
|
|
);
|
|
create function self_ref_trigger_ins_func()
|
|
returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
if new.parent is not null then
|
|
update self_ref_trigger set nchildren = nchildren + 1
|
|
where id = new.parent;
|
|
end if;
|
|
return new;
|
|
end;
|
|
$$;
|
|
create trigger self_ref_trigger_ins_trig before insert on self_ref_trigger
|
|
for each row execute procedure self_ref_trigger_ins_func();
|
|
create function self_ref_trigger_del_func()
|
|
returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
if old.parent is not null then
|
|
update self_ref_trigger set nchildren = nchildren - 1
|
|
where id = old.parent;
|
|
end if;
|
|
return old;
|
|
end;
|
|
$$;
|
|
create trigger self_ref_trigger_del_trig before delete on self_ref_trigger
|
|
for each row execute procedure self_ref_trigger_del_func();
|
|
insert into self_ref_trigger values (1, null, 'root');
|
|
insert into self_ref_trigger values (2, 1, 'root child A');
|
|
insert into self_ref_trigger values (3, 1, 'root child B');
|
|
insert into self_ref_trigger values (4, 2, 'grandchild 1');
|
|
insert into self_ref_trigger values (5, 3, 'grandchild 2');
|
|
update self_ref_trigger set data = 'root!' where id = 1;
|
|
select * from self_ref_trigger;
|
|
id | parent | data | nchildren
|
|
----+--------+--------------+-----------
|
|
2 | 1 | root child A | 1
|
|
4 | 2 | grandchild 1 | 0
|
|
3 | 1 | root child B | 1
|
|
5 | 3 | grandchild 2 | 0
|
|
1 | | root! | 2
|
|
(5 rows)
|
|
|
|
delete from self_ref_trigger;
|
|
ERROR: tuple to be updated was already modified by an operation triggered by the current command
|
|
HINT: Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.
|
|
select * from self_ref_trigger;
|
|
id | parent | data | nchildren
|
|
----+--------+--------------+-----------
|
|
2 | 1 | root child A | 1
|
|
4 | 2 | grandchild 1 | 0
|
|
3 | 1 | root child B | 1
|
|
5 | 3 | grandchild 2 | 0
|
|
1 | | root! | 2
|
|
(5 rows)
|
|
|
|
drop table self_ref_trigger;
|
|
drop function self_ref_trigger_ins_func();
|
|
drop function self_ref_trigger_del_func();
|
|
--
|
|
-- Check that statement triggers work correctly even with all children excluded
|
|
--
|
|
create table stmt_trig_on_empty_upd (a int);
|
|
create table stmt_trig_on_empty_upd1 () inherits (stmt_trig_on_empty_upd);
|
|
create function update_stmt_notice() returns trigger as $$
|
|
begin
|
|
raise notice 'updating %', TG_TABLE_NAME;
|
|
return null;
|
|
end;
|
|
$$ language plpgsql;
|
|
create trigger before_stmt_trigger
|
|
before update on stmt_trig_on_empty_upd
|
|
execute procedure update_stmt_notice();
|
|
create trigger before_stmt_trigger
|
|
before update on stmt_trig_on_empty_upd1
|
|
execute procedure update_stmt_notice();
|
|
-- inherited no-op update
|
|
update stmt_trig_on_empty_upd set a = a where false returning a+1 as aa;
|
|
NOTICE: updating stmt_trig_on_empty_upd
|
|
aa
|
|
----
|
|
(0 rows)
|
|
|
|
-- simple no-op update
|
|
update stmt_trig_on_empty_upd1 set a = a where false returning a+1 as aa;
|
|
NOTICE: updating stmt_trig_on_empty_upd1
|
|
aa
|
|
----
|
|
(0 rows)
|
|
|
|
drop table stmt_trig_on_empty_upd cascade;
|
|
NOTICE: drop cascades to table stmt_trig_on_empty_upd1
|
|
drop function update_stmt_notice();
|
|
--
|
|
-- Check that index creation (or DDL in general) is prohibited in a trigger
|
|
--
|
|
create table trigger_ddl_table (
|
|
col1 integer,
|
|
col2 integer
|
|
);
|
|
create function trigger_ddl_func() returns trigger as $$
|
|
begin
|
|
alter table trigger_ddl_table add primary key (col1);
|
|
return new;
|
|
end$$ language plpgsql;
|
|
create trigger trigger_ddl_func before insert on trigger_ddl_table for each row
|
|
execute procedure trigger_ddl_func();
|
|
insert into trigger_ddl_table values (1, 42); -- fail
|
|
ERROR: cannot ALTER TABLE "trigger_ddl_table" because it is being used by active queries in this session
|
|
CONTEXT: SQL statement "alter table trigger_ddl_table add primary key (col1)"
|
|
PL/pgSQL function trigger_ddl_func() line 3 at SQL statement
|
|
create or replace function trigger_ddl_func() returns trigger as $$
|
|
begin
|
|
create index on trigger_ddl_table (col2);
|
|
return new;
|
|
end$$ language plpgsql;
|
|
insert into trigger_ddl_table values (1, 42); -- fail
|
|
ERROR: cannot CREATE INDEX "trigger_ddl_table" because it is being used by active queries in this session
|
|
CONTEXT: SQL statement "create index on trigger_ddl_table (col2)"
|
|
PL/pgSQL function trigger_ddl_func() line 3 at SQL statement
|
|
drop table trigger_ddl_table;
|
|
drop function trigger_ddl_func();
|
|
--
|
|
-- Verify behavior of before and after triggers with INSERT...ON CONFLICT
|
|
-- DO UPDATE
|
|
--
|
|
create table upsert (key int4 primary key, color text);
|
|
create function upsert_before_func()
|
|
returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
if (TG_OP = 'UPDATE') then
|
|
raise warning 'before update (old): %', old.*::text;
|
|
raise warning 'before update (new): %', new.*::text;
|
|
elsif (TG_OP = 'INSERT') then
|
|
raise warning 'before insert (new): %', new.*::text;
|
|
if new.key % 2 = 0 then
|
|
new.key := new.key + 1;
|
|
new.color := new.color || ' trig modified';
|
|
raise warning 'before insert (new, modified): %', new.*::text;
|
|
end if;
|
|
end if;
|
|
return new;
|
|
end;
|
|
$$;
|
|
create trigger upsert_before_trig before insert or update on upsert
|
|
for each row execute procedure upsert_before_func();
|
|
create function upsert_after_func()
|
|
returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
if (TG_OP = 'UPDATE') then
|
|
raise warning 'after update (old): %', old.*::text;
|
|
raise warning 'after update (new): %', new.*::text;
|
|
elsif (TG_OP = 'INSERT') then
|
|
raise warning 'after insert (new): %', new.*::text;
|
|
end if;
|
|
return null;
|
|
end;
|
|
$$;
|
|
create trigger upsert_after_trig after insert or update on upsert
|
|
for each row execute procedure upsert_after_func();
|
|
insert into upsert values(1, 'black') on conflict (key) do update set color = 'updated ' || upsert.color;
|
|
WARNING: before insert (new): (1,black)
|
|
WARNING: after insert (new): (1,black)
|
|
insert into upsert values(2, 'red') on conflict (key) do update set color = 'updated ' || upsert.color;
|
|
WARNING: before insert (new): (2,red)
|
|
WARNING: before insert (new, modified): (3,"red trig modified")
|
|
WARNING: after insert (new): (3,"red trig modified")
|
|
insert into upsert values(3, 'orange') on conflict (key) do update set color = 'updated ' || upsert.color;
|
|
WARNING: before insert (new): (3,orange)
|
|
WARNING: before update (old): (3,"red trig modified")
|
|
WARNING: before update (new): (3,"updated red trig modified")
|
|
WARNING: after update (old): (3,"red trig modified")
|
|
WARNING: after update (new): (3,"updated red trig modified")
|
|
insert into upsert values(4, 'green') on conflict (key) do update set color = 'updated ' || upsert.color;
|
|
WARNING: before insert (new): (4,green)
|
|
WARNING: before insert (new, modified): (5,"green trig modified")
|
|
WARNING: after insert (new): (5,"green trig modified")
|
|
insert into upsert values(5, 'purple') on conflict (key) do update set color = 'updated ' || upsert.color;
|
|
WARNING: before insert (new): (5,purple)
|
|
WARNING: before update (old): (5,"green trig modified")
|
|
WARNING: before update (new): (5,"updated green trig modified")
|
|
WARNING: after update (old): (5,"green trig modified")
|
|
WARNING: after update (new): (5,"updated green trig modified")
|
|
insert into upsert values(6, 'white') on conflict (key) do update set color = 'updated ' || upsert.color;
|
|
WARNING: before insert (new): (6,white)
|
|
WARNING: before insert (new, modified): (7,"white trig modified")
|
|
WARNING: after insert (new): (7,"white trig modified")
|
|
insert into upsert values(7, 'pink') on conflict (key) do update set color = 'updated ' || upsert.color;
|
|
WARNING: before insert (new): (7,pink)
|
|
WARNING: before update (old): (7,"white trig modified")
|
|
WARNING: before update (new): (7,"updated white trig modified")
|
|
WARNING: after update (old): (7,"white trig modified")
|
|
WARNING: after update (new): (7,"updated white trig modified")
|
|
insert into upsert values(8, 'yellow') on conflict (key) do update set color = 'updated ' || upsert.color;
|
|
WARNING: before insert (new): (8,yellow)
|
|
WARNING: before insert (new, modified): (9,"yellow trig modified")
|
|
WARNING: after insert (new): (9,"yellow trig modified")
|
|
select * from upsert;
|
|
key | color
|
|
-----+-----------------------------
|
|
1 | black
|
|
3 | updated red trig modified
|
|
5 | updated green trig modified
|
|
7 | updated white trig modified
|
|
9 | yellow trig modified
|
|
(5 rows)
|
|
|
|
drop table upsert;
|
|
drop function upsert_before_func();
|
|
drop function upsert_after_func();
|
|
--
|
|
-- Verify that triggers with transition tables are not allowed on
|
|
-- views
|
|
--
|
|
create table my_table (i int);
|
|
create view my_view as select * from my_table;
|
|
create function my_trigger_function() returns trigger as $$ begin end; $$ language plpgsql;
|
|
create trigger my_trigger after update on my_view referencing old table as old_table
|
|
for each statement execute procedure my_trigger_function();
|
|
ERROR: "my_view" is a view
|
|
DETAIL: Triggers on views cannot have transition tables.
|
|
drop function my_trigger_function();
|
|
drop view my_view;
|
|
drop table my_table;
|
|
--
|
|
-- Verify cases that are unsupported with partitioned tables
|
|
--
|
|
create table parted_trig (a int) partition by list (a);
|
|
create function trigger_nothing() returns trigger
|
|
language plpgsql as $$ begin end; $$;
|
|
create trigger failed instead of update on parted_trig
|
|
for each row execute procedure trigger_nothing();
|
|
ERROR: "parted_trig" is a table
|
|
DETAIL: Tables cannot have INSTEAD OF triggers.
|
|
create trigger failed after update on parted_trig
|
|
referencing old table as old_table
|
|
for each row execute procedure trigger_nothing();
|
|
ERROR: "parted_trig" is a partitioned table
|
|
DETAIL: ROW triggers with transition tables are not supported on partitioned tables.
|
|
drop table parted_trig;
|
|
--
|
|
-- Verify trigger creation for partitioned tables, and drop behavior
|
|
--
|
|
create table trigpart (a int, b int) partition by range (a);
|
|
create table trigpart1 partition of trigpart for values from (0) to (1000);
|
|
create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
|
|
create table trigpart2 partition of trigpart for values from (1000) to (2000);
|
|
create table trigpart3 (like trigpart);
|
|
alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
|
|
create table trigpart4 partition of trigpart for values from (3000) to (4000) partition by range (a);
|
|
create table trigpart41 partition of trigpart4 for values from (3000) to (3500);
|
|
create table trigpart42 (like trigpart);
|
|
alter table trigpart4 attach partition trigpart42 for values from (3500) to (4000);
|
|
select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
|
|
where tgrelid::regclass::text like 'trigpart%' order by tgrelid::regclass::text;
|
|
tgrelid | tgname | tgfoid
|
|
------------+--------+-----------------
|
|
trigpart | trg1 | trigger_nothing
|
|
trigpart1 | trg1 | trigger_nothing
|
|
trigpart2 | trg1 | trigger_nothing
|
|
trigpart3 | trg1 | trigger_nothing
|
|
trigpart4 | trg1 | trigger_nothing
|
|
trigpart41 | trg1 | trigger_nothing
|
|
trigpart42 | trg1 | trigger_nothing
|
|
(7 rows)
|
|
|
|
drop trigger trg1 on trigpart1; -- fail
|
|
ERROR: cannot drop trigger trg1 on table trigpart1 because trigger trg1 on table trigpart requires it
|
|
HINT: You can drop trigger trg1 on table trigpart instead.
|
|
drop trigger trg1 on trigpart2; -- fail
|
|
ERROR: cannot drop trigger trg1 on table trigpart2 because trigger trg1 on table trigpart requires it
|
|
HINT: You can drop trigger trg1 on table trigpart instead.
|
|
drop trigger trg1 on trigpart3; -- fail
|
|
ERROR: cannot drop trigger trg1 on table trigpart3 because trigger trg1 on table trigpart requires it
|
|
HINT: You can drop trigger trg1 on table trigpart instead.
|
|
drop table trigpart2; -- ok, trigger should be gone in that partition
|
|
select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
|
|
where tgrelid::regclass::text like 'trigpart%' order by tgrelid::regclass::text;
|
|
tgrelid | tgname | tgfoid
|
|
------------+--------+-----------------
|
|
trigpart | trg1 | trigger_nothing
|
|
trigpart1 | trg1 | trigger_nothing
|
|
trigpart3 | trg1 | trigger_nothing
|
|
trigpart4 | trg1 | trigger_nothing
|
|
trigpart41 | trg1 | trigger_nothing
|
|
trigpart42 | trg1 | trigger_nothing
|
|
(6 rows)
|
|
|
|
drop trigger trg1 on trigpart; -- ok, all gone
|
|
select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
|
|
where tgrelid::regclass::text like 'trigpart%' order by tgrelid::regclass::text;
|
|
tgrelid | tgname | tgfoid
|
|
---------+--------+--------
|
|
(0 rows)
|
|
|
|
-- check detach behavior
|
|
create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
|
|
\d trigpart3
|
|
Table "public.trigpart3"
|
|
Column | Type | Collation | Nullable | Default
|
|
--------+---------+-----------+----------+---------
|
|
a | integer | | |
|
|
b | integer | | |
|
|
Partition of: trigpart FOR VALUES FROM (2000) TO (3000)
|
|
Triggers:
|
|
trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing(), ON TABLE trigpart
|
|
|
|
alter table trigpart detach partition trigpart3;
|
|
drop trigger trg1 on trigpart3; -- fail due to "does not exist"
|
|
ERROR: trigger "trg1" for table "trigpart3" does not exist
|
|
alter table trigpart detach partition trigpart4;
|
|
drop trigger trg1 on trigpart41; -- fail due to "does not exist"
|
|
ERROR: trigger "trg1" for table "trigpart41" does not exist
|
|
drop table trigpart4;
|
|
alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
|
|
alter table trigpart detach partition trigpart3;
|
|
alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
|
|
drop table trigpart3;
|
|
select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
|
|
where tgname ~ '^trg1' order by 1;
|
|
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
|
|
-----------+--------+-----------------+-----------+--------------
|
|
trigpart | trg1 | trigger_nothing | O | f
|
|
trigpart1 | trg1 | trigger_nothing | O | f
|
|
(2 rows)
|
|
|
|
create table trigpart3 (like trigpart);
|
|
create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
|
|
\d trigpart3
|
|
Table "public.trigpart3"
|
|
Column | Type | Collation | Nullable | Default
|
|
--------+---------+-----------+----------+---------
|
|
a | integer | | |
|
|
b | integer | | |
|
|
Triggers:
|
|
trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
|
|
|
|
alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000) to (3000); -- fail
|
|
ERROR: trigger "trg1" for relation "trigpart3" already exists
|
|
drop table trigpart3;
|
|
-- check display of unrelated triggers
|
|
create trigger samename after delete on trigpart execute function trigger_nothing();
|
|
create trigger samename after delete on trigpart1 execute function trigger_nothing();
|
|
\d trigpart1
|
|
Table "public.trigpart1"
|
|
Column | Type | Collation | Nullable | Default
|
|
--------+---------+-----------+----------+---------
|
|
a | integer | | |
|
|
b | integer | | |
|
|
Partition of: trigpart FOR VALUES FROM (0) TO (1000)
|
|
Triggers:
|
|
samename AFTER DELETE ON trigpart1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_nothing()
|
|
trg1 AFTER INSERT ON trigpart1 FOR EACH ROW EXECUTE FUNCTION trigger_nothing(), ON TABLE trigpart
|
|
|
|
drop table trigpart;
|
|
drop function trigger_nothing();
|
|
--
|
|
-- Verify that triggers are fired for partitioned tables
|
|
--
|
|
create table parted_stmt_trig (a int) partition by list (a);
|
|
create table parted_stmt_trig1 partition of parted_stmt_trig for values in (1);
|
|
create table parted_stmt_trig2 partition of parted_stmt_trig for values in (2);
|
|
create table parted2_stmt_trig (a int) partition by list (a);
|
|
create table parted2_stmt_trig1 partition of parted2_stmt_trig for values in (1);
|
|
create table parted2_stmt_trig2 partition of parted2_stmt_trig for values in (2);
|
|
create or replace function trigger_notice() returns trigger as $$
|
|
begin
|
|
raise notice 'trigger % on % % % for %', TG_NAME, TG_TABLE_NAME, TG_WHEN, TG_OP, TG_LEVEL;
|
|
if TG_LEVEL = 'ROW' then
|
|
return NEW;
|
|
end if;
|
|
return null;
|
|
end;
|
|
$$ language plpgsql;
|
|
-- insert/update/delete statement-level triggers on the parent
|
|
create trigger trig_ins_before before insert on parted_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_ins_after after insert on parted_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_upd_before before update on parted_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_upd_after after update on parted_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_del_before before delete on parted_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_del_after after delete on parted_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
-- insert/update/delete row-level triggers on the parent
|
|
create trigger trig_ins_after_parent after insert on parted_stmt_trig
|
|
for each row execute procedure trigger_notice();
|
|
create trigger trig_upd_after_parent after update on parted_stmt_trig
|
|
for each row execute procedure trigger_notice();
|
|
create trigger trig_del_after_parent after delete on parted_stmt_trig
|
|
for each row execute procedure trigger_notice();
|
|
-- insert/update/delete row-level triggers on the first partition
|
|
create trigger trig_ins_before_child before insert on parted_stmt_trig1
|
|
for each row execute procedure trigger_notice();
|
|
create trigger trig_ins_after_child after insert on parted_stmt_trig1
|
|
for each row execute procedure trigger_notice();
|
|
create trigger trig_upd_before_child before update on parted_stmt_trig1
|
|
for each row execute procedure trigger_notice();
|
|
create trigger trig_upd_after_child after update on parted_stmt_trig1
|
|
for each row execute procedure trigger_notice();
|
|
create trigger trig_del_before_child before delete on parted_stmt_trig1
|
|
for each row execute procedure trigger_notice();
|
|
create trigger trig_del_after_child after delete on parted_stmt_trig1
|
|
for each row execute procedure trigger_notice();
|
|
-- insert/update/delete statement-level triggers on the parent
|
|
create trigger trig_ins_before_3 before insert on parted2_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_ins_after_3 after insert on parted2_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_upd_before_3 before update on parted2_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_upd_after_3 after update on parted2_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_del_before_3 before delete on parted2_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger trig_del_after_3 after delete on parted2_stmt_trig
|
|
for each statement execute procedure trigger_notice();
|
|
with ins (a) as (
|
|
insert into parted2_stmt_trig values (1), (2) returning a
|
|
) insert into parted_stmt_trig select a from ins returning tableoid::regclass, a;
|
|
NOTICE: trigger trig_ins_before on parted_stmt_trig BEFORE INSERT for STATEMENT
|
|
NOTICE: trigger trig_ins_before_3 on parted2_stmt_trig BEFORE INSERT for STATEMENT
|
|
NOTICE: trigger trig_ins_before_child on parted_stmt_trig1 BEFORE INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_child on parted_stmt_trig1 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_parent on parted_stmt_trig1 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_parent on parted_stmt_trig2 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_3 on parted2_stmt_trig AFTER INSERT for STATEMENT
|
|
NOTICE: trigger trig_ins_after on parted_stmt_trig AFTER INSERT for STATEMENT
|
|
tableoid | a
|
|
-------------------+---
|
|
parted_stmt_trig1 | 1
|
|
parted_stmt_trig2 | 2
|
|
(2 rows)
|
|
|
|
with upd as (
|
|
update parted2_stmt_trig set a = a
|
|
) update parted_stmt_trig set a = a;
|
|
NOTICE: trigger trig_upd_before on parted_stmt_trig BEFORE UPDATE for STATEMENT
|
|
NOTICE: trigger trig_upd_before_child on parted_stmt_trig1 BEFORE UPDATE for ROW
|
|
NOTICE: trigger trig_upd_before_3 on parted2_stmt_trig BEFORE UPDATE for STATEMENT
|
|
NOTICE: trigger trig_upd_after_child on parted_stmt_trig1 AFTER UPDATE for ROW
|
|
NOTICE: trigger trig_upd_after_parent on parted_stmt_trig1 AFTER UPDATE for ROW
|
|
NOTICE: trigger trig_upd_after_parent on parted_stmt_trig2 AFTER UPDATE for ROW
|
|
NOTICE: trigger trig_upd_after on parted_stmt_trig AFTER UPDATE for STATEMENT
|
|
NOTICE: trigger trig_upd_after_3 on parted2_stmt_trig AFTER UPDATE for STATEMENT
|
|
delete from parted_stmt_trig;
|
|
NOTICE: trigger trig_del_before on parted_stmt_trig BEFORE DELETE for STATEMENT
|
|
NOTICE: trigger trig_del_before_child on parted_stmt_trig1 BEFORE DELETE for ROW
|
|
NOTICE: trigger trig_del_after_parent on parted_stmt_trig2 AFTER DELETE for ROW
|
|
NOTICE: trigger trig_del_after on parted_stmt_trig AFTER DELETE for STATEMENT
|
|
-- insert via copy on the parent
|
|
copy parted_stmt_trig(a) from stdin;
|
|
NOTICE: trigger trig_ins_before on parted_stmt_trig BEFORE INSERT for STATEMENT
|
|
NOTICE: trigger trig_ins_before_child on parted_stmt_trig1 BEFORE INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_child on parted_stmt_trig1 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_parent on parted_stmt_trig1 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_parent on parted_stmt_trig2 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after on parted_stmt_trig AFTER INSERT for STATEMENT
|
|
-- insert via copy on the first partition
|
|
copy parted_stmt_trig1(a) from stdin;
|
|
NOTICE: trigger trig_ins_before_child on parted_stmt_trig1 BEFORE INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_child on parted_stmt_trig1 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_parent on parted_stmt_trig1 AFTER INSERT for ROW
|
|
-- Disabling a trigger in the parent table should disable children triggers too
|
|
alter table parted_stmt_trig disable trigger trig_ins_after_parent;
|
|
insert into parted_stmt_trig values (1);
|
|
NOTICE: trigger trig_ins_before on parted_stmt_trig BEFORE INSERT for STATEMENT
|
|
NOTICE: trigger trig_ins_before_child on parted_stmt_trig1 BEFORE INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_child on parted_stmt_trig1 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after on parted_stmt_trig AFTER INSERT for STATEMENT
|
|
alter table parted_stmt_trig enable trigger trig_ins_after_parent;
|
|
insert into parted_stmt_trig values (1);
|
|
NOTICE: trigger trig_ins_before on parted_stmt_trig BEFORE INSERT for STATEMENT
|
|
NOTICE: trigger trig_ins_before_child on parted_stmt_trig1 BEFORE INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_child on parted_stmt_trig1 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after_parent on parted_stmt_trig1 AFTER INSERT for ROW
|
|
NOTICE: trigger trig_ins_after on parted_stmt_trig AFTER INSERT for STATEMENT
|
|
drop table parted_stmt_trig, parted2_stmt_trig;
|
|
-- Verify that triggers fire in alphabetical order
|
|
create table parted_trig (a int) partition by range (a);
|
|
create table parted_trig_1 partition of parted_trig for values from (0) to (1000)
|
|
partition by range (a);
|
|
create table parted_trig_1_1 partition of parted_trig_1 for values from (0) to (100);
|
|
create table parted_trig_2 partition of parted_trig for values from (1000) to (2000);
|
|
create trigger zzz after insert on parted_trig for each row execute procedure trigger_notice();
|
|
create trigger mmm after insert on parted_trig_1_1 for each row execute procedure trigger_notice();
|
|
create trigger aaa after insert on parted_trig_1 for each row execute procedure trigger_notice();
|
|
create trigger bbb after insert on parted_trig for each row execute procedure trigger_notice();
|
|
create trigger qqq after insert on parted_trig_1_1 for each row execute procedure trigger_notice();
|
|
insert into parted_trig values (50), (1500);
|
|
NOTICE: trigger aaa on parted_trig_1_1 AFTER INSERT for ROW
|
|
NOTICE: trigger bbb on parted_trig_1_1 AFTER INSERT for ROW
|
|
NOTICE: trigger mmm on parted_trig_1_1 AFTER INSERT for ROW
|
|
NOTICE: trigger qqq on parted_trig_1_1 AFTER INSERT for ROW
|
|
NOTICE: trigger zzz on parted_trig_1_1 AFTER INSERT for ROW
|
|
NOTICE: trigger bbb on parted_trig_2 AFTER INSERT for ROW
|
|
NOTICE: trigger zzz on parted_trig_2 AFTER INSERT for ROW
|
|
drop table parted_trig;
|
|
-- Verify that the correct triggers fire for cross-partition updates
|
|
create table parted_trig (a int) partition by list (a);
|
|
create table parted_trig1 partition of parted_trig for values in (1);
|
|
create table parted_trig2 partition of parted_trig for values in (2);
|
|
insert into parted_trig values (1);
|
|
create or replace function trigger_notice() returns trigger as $$
|
|
begin
|
|
raise notice 'trigger % on % % % for %', TG_NAME, TG_TABLE_NAME, TG_WHEN, TG_OP, TG_LEVEL;
|
|
if TG_LEVEL = 'ROW' then
|
|
if TG_OP = 'DELETE' then
|
|
return OLD;
|
|
else
|
|
return NEW;
|
|
end if;
|
|
end if;
|
|
return null;
|
|
end;
|
|
$$ language plpgsql;
|
|
create trigger parted_trig_before_stmt before insert or update or delete on parted_trig
|
|
for each statement execute procedure trigger_notice();
|
|
create trigger parted_trig_before_row before insert or update or delete on parted_trig
|
|
for each row execute procedure trigger_notice();
|
|
create trigger parted_trig_after_row after insert or update or delete on parted_trig
|
|
for each row execute procedure trigger_notice();
|
|
create trigger parted_trig_after_stmt after insert or update or delete on parted_trig
|
|
for each statement execute procedure trigger_notice();
|
|
update parted_trig set a = 2 where a = 1;
|
|
NOTICE: trigger parted_trig_before_stmt on parted_trig BEFORE UPDATE for STATEMENT
|
|
NOTICE: trigger parted_trig_before_row on parted_trig1 BEFORE UPDATE for ROW
|
|
NOTICE: trigger parted_trig_before_row on parted_trig1 BEFORE DELETE for ROW
|
|
NOTICE: trigger parted_trig_before_row on parted_trig2 BEFORE INSERT for ROW
|
|
NOTICE: trigger parted_trig_after_row on parted_trig1 AFTER DELETE for ROW
|
|
NOTICE: trigger parted_trig_after_row on parted_trig2 AFTER INSERT for ROW
|
|
NOTICE: trigger parted_trig_after_stmt on parted_trig AFTER UPDATE for STATEMENT
|
|
-- update action in merge should behave the same
|
|
merge into parted_trig using (select 1) as ss on true
|
|
when matched and a = 2 then update set a = 1;
|
|
NOTICE: trigger parted_trig_before_stmt on parted_trig BEFORE UPDATE for STATEMENT
|
|
NOTICE: trigger parted_trig_before_row on parted_trig2 BEFORE UPDATE for ROW
|
|
NOTICE: trigger parted_trig_before_row on parted_trig2 BEFORE DELETE for ROW
|
|
NOTICE: trigger parted_trig_before_row on parted_trig1 BEFORE INSERT for ROW
|
|
NOTICE: trigger parted_trig_after_row on parted_trig2 AFTER DELETE for ROW
|
|
NOTICE: trigger parted_trig_after_row on parted_trig1 AFTER INSERT for ROW
|
|
NOTICE: trigger parted_trig_after_stmt on parted_trig AFTER UPDATE for STATEMENT
|
|
drop table parted_trig;
|
|
-- Verify propagation of trigger arguments to partitions
|
|
create table parted_trig (a int) partition by list (a);
|
|
create table parted_trig1 partition of parted_trig for values in (1);
|
|
create or replace function trigger_notice() returns trigger as $$
|
|
declare
|
|
arg1 text = TG_ARGV[0];
|
|
arg2 integer = TG_ARGV[1];
|
|
begin
|
|
raise notice 'trigger % on % % % for % args % %',
|
|
TG_NAME, TG_TABLE_NAME, TG_WHEN, TG_OP, TG_LEVEL, arg1, arg2;
|
|
return null;
|
|
end;
|
|
$$ language plpgsql;
|
|
create trigger aaa after insert on parted_trig
|
|
for each row execute procedure trigger_notice('quirky', 1);
|
|
-- Verify propagation of trigger arguments to partitions attached after creating trigger
|
|
create table parted_trig2 partition of parted_trig for values in (2);
|
|
create table parted_trig3 (like parted_trig);
|
|
alter table parted_trig attach partition parted_trig3 for values in (3);
|
|
insert into parted_trig values (1), (2), (3);
|
|
NOTICE: trigger aaa on parted_trig1 AFTER INSERT for ROW args quirky 1
|
|
NOTICE: trigger aaa on parted_trig2 AFTER INSERT for ROW args quirky 1
|
|
NOTICE: trigger aaa on parted_trig3 AFTER INSERT for ROW args quirky 1
|
|
drop table parted_trig;
|
|
-- test irregular partitions (i.e., different column definitions),
|
|
-- including that the WHEN clause works
|
|
create function bark(text) returns bool language plpgsql immutable
|
|
as $$ begin raise notice '% <- woof!', $1; return true; end; $$;
|
|
create or replace function trigger_notice_ab() returns trigger as $$
|
|
begin
|
|
raise notice 'trigger % on % % % for %: (a,b)=(%,%)',
|
|
TG_NAME, TG_TABLE_NAME, TG_WHEN, TG_OP, TG_LEVEL,
|
|
NEW.a, NEW.b;
|
|
if TG_LEVEL = 'ROW' then
|
|
return NEW;
|
|
end if;
|
|
return null;
|
|
end;
|
|
$$ language plpgsql;
|
|
create table parted_irreg_ancestor (fd text, b text, fd2 int, fd3 int, a int)
|
|
partition by range (b);
|
|
alter table parted_irreg_ancestor drop column fd,
|
|
drop column fd2, drop column fd3;
|
|
create table parted_irreg (fd int, a int, fd2 int, b text)
|
|
partition by range (b);
|
|
alter table parted_irreg drop column fd, drop column fd2;
|
|
alter table parted_irreg_ancestor attach partition parted_irreg
|
|
for values from ('aaaa') to ('zzzz');
|
|
create table parted1_irreg (b text, fd int, a int);
|
|
alter table parted1_irreg drop column fd;
|
|
alter table parted_irreg attach partition parted1_irreg
|
|
for values from ('aaaa') to ('bbbb');
|
|
create trigger parted_trig after insert on parted_irreg
|
|
for each row execute procedure trigger_notice_ab();
|
|
create trigger parted_trig_odd after insert on parted_irreg for each row
|
|
when (bark(new.b) AND new.a % 2 = 1) execute procedure trigger_notice_ab();
|
|
-- we should hear barking for every insert, but parted_trig_odd only emits
|
|
-- noise for odd values of a. parted_trig does it for all inserts.
|
|
insert into parted_irreg values (1, 'aardvark'), (2, 'aanimals');
|
|
NOTICE: aardvark <- woof!
|
|
NOTICE: aanimals <- woof!
|
|
NOTICE: trigger parted_trig on parted1_irreg AFTER INSERT for ROW: (a,b)=(1,aardvark)
|
|
NOTICE: trigger parted_trig_odd on parted1_irreg AFTER INSERT for ROW: (a,b)=(1,aardvark)
|
|
NOTICE: trigger parted_trig on parted1_irreg AFTER INSERT for ROW: (a,b)=(2,aanimals)
|
|
insert into parted1_irreg values ('aardwolf', 2);
|
|
NOTICE: aardwolf <- woof!
|
|
NOTICE: trigger parted_trig on parted1_irreg AFTER INSERT for ROW: (a,b)=(2,aardwolf)
|
|
insert into parted_irreg_ancestor values ('aasvogel', 3);
|
|
NOTICE: aasvogel <- woof!
|
|
NOTICE: trigger parted_trig on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel)
|
|
NOTICE: trigger parted_trig_odd on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel)
|
|
drop table parted_irreg_ancestor;
|
|
-- Before triggers and partitions
|
|
create table parted (a int, b int, c text) partition by list (a);
|
|
create table parted_1 partition of parted for values in (1)
|
|
partition by list (b);
|
|
create table parted_1_1 partition of parted_1 for values in (1);
|
|
create function parted_trigfunc() returns trigger language plpgsql as $$
|
|
begin
|
|
new.a = new.a + 1;
|
|
return new;
|
|
end;
|
|
$$;
|
|
insert into parted values (1, 1, 'uno uno v1'); -- works
|
|
create trigger t before insert or update or delete on parted
|
|
for each row execute function parted_trigfunc();
|
|
insert into parted values (1, 1, 'uno uno v2'); -- fail
|
|
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
|
|
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
|
|
update parted set c = c || 'v3'; -- fail
|
|
ERROR: no partition of relation "parted" found for row
|
|
DETAIL: Partition key of the failing row contains (a) = (2).
|
|
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
|
|
begin
|
|
new.b = new.b + 1;
|
|
return new;
|
|
end;
|
|
$$;
|
|
insert into parted values (1, 1, 'uno uno v4'); -- fail
|
|
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
|
|
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
|
|
update parted set c = c || 'v5'; -- fail
|
|
ERROR: no partition of relation "parted_1" found for row
|
|
DETAIL: Partition key of the failing row contains (b) = (2).
|
|
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
|
|
begin
|
|
new.c = new.c || ' did '|| TG_OP;
|
|
return new;
|
|
end;
|
|
$$;
|
|
insert into parted values (1, 1, 'uno uno'); -- works
|
|
update parted set c = c || ' v6'; -- works
|
|
select tableoid::regclass, * from parted;
|
|
tableoid | a | b | c
|
|
------------+---+---+----------------------------------
|
|
parted_1_1 | 1 | 1 | uno uno v1 v6 did UPDATE
|
|
parted_1_1 | 1 | 1 | uno uno did INSERT v6 did UPDATE
|
|
(2 rows)
|
|
|
|
-- update itself moves tuple to new partition; trigger still works
|
|
truncate table parted;
|
|
create table parted_2 partition of parted for values in (2);
|
|
insert into parted values (1, 1, 'uno uno v5');
|
|
update parted set a = 2;
|
|
select tableoid::regclass, * from parted;
|
|
tableoid | a | b | c
|
|
----------+---+---+---------------------------------------------
|
|
parted_2 | 2 | 1 | uno uno v5 did INSERT did UPDATE did INSERT
|
|
(1 row)
|
|
|
|
-- both trigger and update change the partition
|
|
create or replace function parted_trigfunc2() returns trigger language plpgsql as $$
|
|
begin
|
|
new.a = new.a + 1;
|
|
return new;
|
|
end;
|
|
$$;
|
|
create trigger t2 before update on parted
|
|
for each row execute function parted_trigfunc2();
|
|
truncate table parted;
|
|
insert into parted values (1, 1, 'uno uno v6');
|
|
create table parted_3 partition of parted for values in (3);
|
|
update parted set a = a + 1;
|
|
select tableoid::regclass, * from parted;
|
|
tableoid | a | b | c
|
|
----------+---+---+---------------------------------------------
|
|
parted_3 | 3 | 1 | uno uno v6 did INSERT did UPDATE did INSERT
|
|
(1 row)
|
|
|
|
-- there's no partition for a=0, but this update works anyway because
|
|
-- the trigger causes the tuple to be routed to another partition
|
|
update parted set a = 0;
|
|
select tableoid::regclass, * from parted;
|
|
tableoid | a | b | c
|
|
------------+---+---+-------------------------------------------------------------------
|
|
parted_1_1 | 1 | 1 | uno uno v6 did INSERT did UPDATE did INSERT did UPDATE did INSERT
|
|
(1 row)
|
|
|
|
drop table parted;
|
|
create table parted (a int, b int, c text) partition by list ((a + b));
|
|
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
|
|
begin
|
|
new.a = new.a + new.b;
|
|
return new;
|
|
end;
|
|
$$;
|
|
create table parted_1 partition of parted for values in (1, 2);
|
|
create table parted_2 partition of parted for values in (3, 4);
|
|
create trigger t before insert or update on parted
|
|
for each row execute function parted_trigfunc();
|
|
insert into parted values (0, 1, 'zero win');
|
|
insert into parted values (1, 1, 'one fail');
|
|
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
|
|
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1".
|
|
insert into parted values (1, 2, 'two fail');
|
|
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
|
|
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_2".
|
|
select * from parted;
|
|
a | b | c
|
|
---+---+----------
|
|
1 | 1 | zero win
|
|
(1 row)
|
|
|
|
drop table parted;
|
|
drop function parted_trigfunc();
|
|
--
|
|
-- Constraint triggers and partitioned tables
|
|
create table parted_constr_ancestor (a int, b text)
|
|
partition by range (b);
|
|
create table parted_constr (a int, b text)
|
|
partition by range (b);
|
|
alter table parted_constr_ancestor attach partition parted_constr
|
|
for values from ('aaaa') to ('zzzz');
|
|
create table parted1_constr (a int, b text);
|
|
alter table parted_constr attach partition parted1_constr
|
|
for values from ('aaaa') to ('bbbb');
|
|
create constraint trigger parted_trig after insert on parted_constr_ancestor
|
|
deferrable
|
|
for each row execute procedure trigger_notice_ab();
|
|
create constraint trigger parted_trig_two after insert on parted_constr
|
|
deferrable initially deferred
|
|
for each row when (bark(new.b) AND new.a % 2 = 1)
|
|
execute procedure trigger_notice_ab();
|
|
-- The immediate constraint is fired immediately; the WHEN clause of the
|
|
-- deferred constraint is also called immediately. The deferred constraint
|
|
-- is fired at commit time.
|
|
begin;
|
|
insert into parted_constr values (1, 'aardvark');
|
|
NOTICE: aardvark <- woof!
|
|
NOTICE: trigger parted_trig on parted1_constr AFTER INSERT for ROW: (a,b)=(1,aardvark)
|
|
insert into parted1_constr values (2, 'aardwolf');
|
|
NOTICE: aardwolf <- woof!
|
|
NOTICE: trigger parted_trig on parted1_constr AFTER INSERT for ROW: (a,b)=(2,aardwolf)
|
|
insert into parted_constr_ancestor values (3, 'aasvogel');
|
|
NOTICE: aasvogel <- woof!
|
|
NOTICE: trigger parted_trig on parted1_constr AFTER INSERT for ROW: (a,b)=(3,aasvogel)
|
|
commit;
|
|
NOTICE: trigger parted_trig_two on parted1_constr AFTER INSERT for ROW: (a,b)=(1,aardvark)
|
|
NOTICE: trigger parted_trig_two on parted1_constr AFTER INSERT for ROW: (a,b)=(3,aasvogel)
|
|
-- The WHEN clause is immediate, and both constraint triggers are fired at
|
|
-- commit time.
|
|
begin;
|
|
set constraints parted_trig deferred;
|
|
insert into parted_constr values (1, 'aardvark');
|
|
NOTICE: aardvark <- woof!
|
|
insert into parted1_constr values (2, 'aardwolf'), (3, 'aasvogel');
|
|
NOTICE: aardwolf <- woof!
|
|
NOTICE: aasvogel <- woof!
|
|
commit;
|
|
NOTICE: trigger parted_trig on parted1_constr AFTER INSERT for ROW: (a,b)=(1,aardvark)
|
|
NOTICE: trigger parted_trig_two on parted1_constr AFTER INSERT for ROW: (a,b)=(1,aardvark)
|
|
NOTICE: trigger parted_trig on parted1_constr AFTER INSERT for ROW: (a,b)=(2,aardwolf)
|
|
NOTICE: trigger parted_trig on parted1_constr AFTER INSERT for ROW: (a,b)=(3,aasvogel)
|
|
NOTICE: trigger parted_trig_two on parted1_constr AFTER INSERT for ROW: (a,b)=(3,aasvogel)
|
|
drop table parted_constr_ancestor;
|
|
drop function bark(text);
|
|
-- Test that the WHEN clause is set properly to partitions
|
|
create table parted_trigger (a int, b text) partition by range (a);
|
|
create table parted_trigger_1 partition of parted_trigger for values from (0) to (1000);
|
|
create table parted_trigger_2 (drp int, a int, b text);
|
|
alter table parted_trigger_2 drop column drp;
|
|
alter table parted_trigger attach partition parted_trigger_2 for values from (1000) to (2000);
|
|
create trigger parted_trigger after update on parted_trigger
|
|
for each row when (new.a % 2 = 1 and length(old.b) >= 2) execute procedure trigger_notice_ab();
|
|
create table parted_trigger_3 (b text, a int) partition by range (length(b));
|
|
create table parted_trigger_3_1 partition of parted_trigger_3 for values from (1) to (3);
|
|
create table parted_trigger_3_2 partition of parted_trigger_3 for values from (3) to (5);
|
|
alter table parted_trigger attach partition parted_trigger_3 for values from (2000) to (3000);
|
|
insert into parted_trigger values
|
|
(0, 'a'), (1, 'bbb'), (2, 'bcd'), (3, 'c'),
|
|
(1000, 'c'), (1001, 'ddd'), (1002, 'efg'), (1003, 'f'),
|
|
(2000, 'e'), (2001, 'fff'), (2002, 'ghi'), (2003, 'h');
|
|
update parted_trigger set a = a + 2; -- notice for odd 'a' values, long 'b' values
|
|
NOTICE: trigger parted_trigger on parted_trigger_1 AFTER UPDATE for ROW: (a,b)=(3,bbb)
|
|
NOTICE: trigger parted_trigger on parted_trigger_2 AFTER UPDATE for ROW: (a,b)=(1003,ddd)
|
|
NOTICE: trigger parted_trigger on parted_trigger_3_2 AFTER UPDATE for ROW: (a,b)=(2003,fff)
|
|
drop table parted_trigger;
|
|
-- try a constraint trigger, also
|
|
create table parted_referenced (a int);
|
|
create table unparted_trigger (a int, b text); -- for comparison purposes
|
|
create table parted_trigger (a int, b text) partition by range (a);
|
|
create table parted_trigger_1 partition of parted_trigger for values from (0) to (1000);
|
|
create table parted_trigger_2 (drp int, a int, b text);
|
|
alter table parted_trigger_2 drop column drp;
|
|
alter table parted_trigger attach partition parted_trigger_2 for values from (1000) to (2000);
|
|
create constraint trigger parted_trigger after update on parted_trigger
|
|
from parted_referenced
|
|
for each row execute procedure trigger_notice_ab();
|
|
create constraint trigger parted_trigger after update on unparted_trigger
|
|
from parted_referenced
|
|
for each row execute procedure trigger_notice_ab();
|
|
create table parted_trigger_3 (b text, a int) partition by range (length(b));
|
|
create table parted_trigger_3_1 partition of parted_trigger_3 for values from (1) to (3);
|
|
create table parted_trigger_3_2 partition of parted_trigger_3 for values from (3) to (5);
|
|
alter table parted_trigger attach partition parted_trigger_3 for values from (2000) to (3000);
|
|
select tgname, conname, t.tgrelid::regclass, t.tgconstrrelid::regclass,
|
|
c.conrelid::regclass, c.confrelid::regclass
|
|
from pg_trigger t join pg_constraint c on (t.tgconstraint = c.oid)
|
|
where tgname = 'parted_trigger'
|
|
order by t.tgrelid::regclass::text;
|
|
tgname | conname | tgrelid | tgconstrrelid | conrelid | confrelid
|
|
----------------+----------------+--------------------+-------------------+--------------------+-----------
|
|
parted_trigger | parted_trigger | parted_trigger | parted_referenced | parted_trigger | -
|
|
parted_trigger | parted_trigger | parted_trigger_1 | parted_referenced | parted_trigger_1 | -
|
|
parted_trigger | parted_trigger | parted_trigger_2 | parted_referenced | parted_trigger_2 | -
|
|
parted_trigger | parted_trigger | parted_trigger_3 | parted_referenced | parted_trigger_3 | -
|
|
parted_trigger | parted_trigger | parted_trigger_3_1 | parted_referenced | parted_trigger_3_1 | -
|
|
parted_trigger | parted_trigger | parted_trigger_3_2 | parted_referenced | parted_trigger_3_2 | -
|
|
parted_trigger | parted_trigger | unparted_trigger | parted_referenced | unparted_trigger | -
|
|
(7 rows)
|
|
|
|
drop table parted_referenced, parted_trigger, unparted_trigger;
|
|
-- verify that the "AFTER UPDATE OF columns" event is propagated correctly
|
|
create table parted_trigger (a int, b text) partition by range (a);
|
|
create table parted_trigger_1 partition of parted_trigger for values from (0) to (1000);
|
|
create table parted_trigger_2 (drp int, a int, b text);
|
|
alter table parted_trigger_2 drop column drp;
|
|
alter table parted_trigger attach partition parted_trigger_2 for values from (1000) to (2000);
|
|
create trigger parted_trigger after update of b on parted_trigger
|
|
for each row execute procedure trigger_notice_ab();
|
|
create table parted_trigger_3 (b text, a int) partition by range (length(b));
|
|
create table parted_trigger_3_1 partition of parted_trigger_3 for values from (1) to (4);
|
|
create table parted_trigger_3_2 partition of parted_trigger_3 for values from (4) to (8);
|
|
alter table parted_trigger attach partition parted_trigger_3 for values from (2000) to (3000);
|
|
insert into parted_trigger values (0, 'a'), (1000, 'c'), (2000, 'e'), (2001, 'eeee');
|
|
update parted_trigger set a = a + 2; -- no notices here
|
|
update parted_trigger set b = b || 'b'; -- all triggers should fire
|
|
NOTICE: trigger parted_trigger on parted_trigger_1 AFTER UPDATE for ROW: (a,b)=(2,ab)
|
|
NOTICE: trigger parted_trigger on parted_trigger_2 AFTER UPDATE for ROW: (a,b)=(1002,cb)
|
|
NOTICE: trigger parted_trigger on parted_trigger_3_1 AFTER UPDATE for ROW: (a,b)=(2002,eb)
|
|
NOTICE: trigger parted_trigger on parted_trigger_3_2 AFTER UPDATE for ROW: (a,b)=(2003,eeeeb)
|
|
drop table parted_trigger;
|
|
drop function trigger_notice_ab();
|
|
-- Make sure we don't end up with unnecessary copies of triggers, when
|
|
-- cloning them.
|
|
create table trg_clone (a int) partition by range (a);
|
|
create table trg_clone1 partition of trg_clone for values from (0) to (1000);
|
|
alter table trg_clone add constraint uniq unique (a) deferrable;
|
|
create table trg_clone2 partition of trg_clone for values from (1000) to (2000);
|
|
create table trg_clone3 partition of trg_clone for values from (2000) to (3000)
|
|
partition by range (a);
|
|
create table trg_clone_3_3 partition of trg_clone3 for values from (2000) to (2100);
|
|
select tgrelid::regclass, count(*) from pg_trigger
|
|
where tgrelid::regclass in ('trg_clone', 'trg_clone1', 'trg_clone2',
|
|
'trg_clone3', 'trg_clone_3_3')
|
|
group by tgrelid::regclass order by tgrelid::regclass;
|
|
tgrelid | count
|
|
---------------+-------
|
|
trg_clone | 1
|
|
trg_clone1 | 1
|
|
trg_clone2 | 1
|
|
trg_clone3 | 1
|
|
trg_clone_3_3 | 1
|
|
(5 rows)
|
|
|
|
drop table trg_clone;
|
|
-- Test the interaction between ALTER TABLE .. DISABLE TRIGGER and
|
|
-- both kinds of inheritance. Historically, legacy inheritance has
|
|
-- not recursed to children, so that behavior is preserved.
|
|
create table parent (a int);
|
|
create table child1 () inherits (parent);
|
|
create function trig_nothing() returns trigger language plpgsql
|
|
as $$ begin return null; end $$;
|
|
create trigger tg after insert on parent
|
|
for each row execute function trig_nothing();
|
|
create trigger tg after insert on child1
|
|
for each row execute function trig_nothing();
|
|
alter table parent disable trigger tg;
|
|
select tgrelid::regclass, tgname, tgenabled from pg_trigger
|
|
where tgrelid in ('parent'::regclass, 'child1'::regclass)
|
|
order by tgrelid::regclass::text;
|
|
tgrelid | tgname | tgenabled
|
|
---------+--------+-----------
|
|
child1 | tg | O
|
|
parent | tg | D
|
|
(2 rows)
|
|
|
|
alter table only parent enable always trigger tg;
|
|
select tgrelid::regclass, tgname, tgenabled from pg_trigger
|
|
where tgrelid in ('parent'::regclass, 'child1'::regclass)
|
|
order by tgrelid::regclass::text;
|
|
tgrelid | tgname | tgenabled
|
|
---------+--------+-----------
|
|
child1 | tg | O
|
|
parent | tg | A
|
|
(2 rows)
|
|
|
|
drop table parent, child1;
|
|
create table parent (a int) partition by list (a);
|
|
create table child1 partition of parent for values in (1);
|
|
create trigger tg after insert on parent
|
|
for each row execute procedure trig_nothing();
|
|
create trigger tg_stmt after insert on parent
|
|
for statement execute procedure trig_nothing();
|
|
select tgrelid::regclass, tgname, tgenabled from pg_trigger
|
|
where tgrelid in ('parent'::regclass, 'child1'::regclass)
|
|
order by tgrelid::regclass::text, tgname;
|
|
tgrelid | tgname | tgenabled
|
|
---------+---------+-----------
|
|
child1 | tg | O
|
|
parent | tg | O
|
|
parent | tg_stmt | O
|
|
(3 rows)
|
|
|
|
alter table only parent enable always trigger tg; -- no recursion because ONLY
|
|
alter table parent enable always trigger tg_stmt; -- no recursion because statement trigger
|
|
select tgrelid::regclass, tgname, tgenabled from pg_trigger
|
|
where tgrelid in ('parent'::regclass, 'child1'::regclass)
|
|
order by tgrelid::regclass::text, tgname;
|
|
tgrelid | tgname | tgenabled
|
|
---------+---------+-----------
|
|
child1 | tg | O
|
|
parent | tg | A
|
|
parent | tg_stmt | A
|
|
(3 rows)
|
|
|
|
-- The following is a no-op for the parent trigger but not so
|
|
-- for the child trigger, so recursion should be applied.
|
|
alter table parent enable always trigger tg;
|
|
select tgrelid::regclass, tgname, tgenabled from pg_trigger
|
|
where tgrelid in ('parent'::regclass, 'child1'::regclass)
|
|
order by tgrelid::regclass::text, tgname;
|
|
tgrelid | tgname | tgenabled
|
|
---------+---------+-----------
|
|
child1 | tg | A
|
|
parent | tg | A
|
|
parent | tg_stmt | A
|
|
(3 rows)
|
|
|
|
-- This variant malfunctioned in some releases.
|
|
alter table parent disable trigger user;
|
|
select tgrelid::regclass, tgname, tgenabled from pg_trigger
|
|
where tgrelid in ('parent'::regclass, 'child1'::regclass)
|
|
order by tgrelid::regclass::text, tgname;
|
|
tgrelid | tgname | tgenabled
|
|
---------+---------+-----------
|
|
child1 | tg | D
|
|
parent | tg | D
|
|
parent | tg_stmt | D
|
|
(3 rows)
|
|
|
|
drop table parent, child1;
|
|
-- Check processing of foreign key triggers
|
|
create table parent (a int primary key, f int references parent)
|
|
partition by list (a);
|
|
create table child1 partition of parent for values in (1);
|
|
select tgrelid::regclass, rtrim(tgname, '0123456789') as tgname,
|
|
tgfoid::regproc, tgenabled
|
|
from pg_trigger where tgrelid in ('parent'::regclass, 'child1'::regclass)
|
|
order by tgrelid::regclass::text, tgfoid;
|
|
tgrelid | tgname | tgfoid | tgenabled
|
|
---------+-------------------------+------------------------+-----------
|
|
child1 | RI_ConstraintTrigger_c_ | "RI_FKey_check_ins" | O
|
|
child1 | RI_ConstraintTrigger_c_ | "RI_FKey_check_upd" | O
|
|
parent | RI_ConstraintTrigger_c_ | "RI_FKey_check_ins" | O
|
|
parent | RI_ConstraintTrigger_c_ | "RI_FKey_check_upd" | O
|
|
parent | RI_ConstraintTrigger_a_ | "RI_FKey_noaction_del" | O
|
|
parent | RI_ConstraintTrigger_a_ | "RI_FKey_noaction_upd" | O
|
|
(6 rows)
|
|
|
|
alter table parent disable trigger all;
|
|
select tgrelid::regclass, rtrim(tgname, '0123456789') as tgname,
|
|
tgfoid::regproc, tgenabled
|
|
from pg_trigger where tgrelid in ('parent'::regclass, 'child1'::regclass)
|
|
order by tgrelid::regclass::text, tgfoid;
|
|
tgrelid | tgname | tgfoid | tgenabled
|
|
---------+-------------------------+------------------------+-----------
|
|
child1 | RI_ConstraintTrigger_c_ | "RI_FKey_check_ins" | D
|
|
child1 | RI_ConstraintTrigger_c_ | "RI_FKey_check_upd" | D
|
|
parent | RI_ConstraintTrigger_c_ | "RI_FKey_check_ins" | D
|
|
parent | RI_ConstraintTrigger_c_ | "RI_FKey_check_upd" | D
|
|
parent | RI_ConstraintTrigger_a_ | "RI_FKey_noaction_del" | D
|
|
parent | RI_ConstraintTrigger_a_ | "RI_FKey_noaction_upd" | D
|
|
(6 rows)
|
|
|
|
drop table parent, child1;
|
|
-- Verify that firing state propagates correctly on creation, too
|
|
CREATE TABLE trgfire (i int) PARTITION BY RANGE (i);
|
|
CREATE TABLE trgfire1 PARTITION OF trgfire FOR VALUES FROM (1) TO (10);
|
|
CREATE OR REPLACE FUNCTION tgf() RETURNS trigger LANGUAGE plpgsql
|
|
AS $$ begin raise exception 'except'; end $$;
|
|
CREATE TRIGGER tg AFTER INSERT ON trgfire FOR EACH ROW EXECUTE FUNCTION tgf();
|
|
INSERT INTO trgfire VALUES (1);
|
|
ERROR: except
|
|
CONTEXT: PL/pgSQL function tgf() line 1 at RAISE
|
|
ALTER TABLE trgfire DISABLE TRIGGER tg;
|
|
INSERT INTO trgfire VALUES (1);
|
|
CREATE TABLE trgfire2 PARTITION OF trgfire FOR VALUES FROM (10) TO (20);
|
|
INSERT INTO trgfire VALUES (11);
|
|
CREATE TABLE trgfire3 (LIKE trgfire);
|
|
ALTER TABLE trgfire ATTACH PARTITION trgfire3 FOR VALUES FROM (20) TO (30);
|
|
INSERT INTO trgfire VALUES (21);
|
|
CREATE TABLE trgfire4 PARTITION OF trgfire FOR VALUES FROM (30) TO (40) PARTITION BY LIST (i);
|
|
CREATE TABLE trgfire4_30 PARTITION OF trgfire4 FOR VALUES IN (30);
|
|
INSERT INTO trgfire VALUES (30);
|
|
CREATE TABLE trgfire5 (LIKE trgfire) PARTITION BY LIST (i);
|
|
CREATE TABLE trgfire5_40 PARTITION OF trgfire5 FOR VALUES IN (40);
|
|
ALTER TABLE trgfire ATTACH PARTITION trgfire5 FOR VALUES FROM (40) TO (50);
|
|
INSERT INTO trgfire VALUES (40);
|
|
SELECT tgrelid::regclass, tgenabled FROM pg_trigger
|
|
WHERE tgrelid::regclass IN (SELECT oid from pg_class where relname LIKE 'trgfire%')
|
|
ORDER BY tgrelid::regclass::text;
|
|
tgrelid | tgenabled
|
|
-------------+-----------
|
|
trgfire | D
|
|
trgfire1 | D
|
|
trgfire2 | D
|
|
trgfire3 | D
|
|
trgfire4 | D
|
|
trgfire4_30 | D
|
|
trgfire5 | D
|
|
trgfire5_40 | D
|
|
(8 rows)
|
|
|
|
ALTER TABLE trgfire ENABLE TRIGGER tg;
|
|
INSERT INTO trgfire VALUES (1);
|
|
ERROR: except
|
|
CONTEXT: PL/pgSQL function tgf() line 1 at RAISE
|
|
INSERT INTO trgfire VALUES (11);
|
|
ERROR: except
|
|
CONTEXT: PL/pgSQL function tgf() line 1 at RAISE
|
|
INSERT INTO trgfire VALUES (21);
|
|
ERROR: except
|
|
CONTEXT: PL/pgSQL function tgf() line 1 at RAISE
|
|
INSERT INTO trgfire VALUES (30);
|
|
ERROR: except
|
|
CONTEXT: PL/pgSQL function tgf() line 1 at RAISE
|
|
INSERT INTO trgfire VALUES (40);
|
|
ERROR: except
|
|
CONTEXT: PL/pgSQL function tgf() line 1 at RAISE
|
|
DROP TABLE trgfire;
|
|
DROP FUNCTION tgf();
|
|
--
|
|
-- Test the interaction between transition tables and both kinds of
|
|
-- inheritance. We'll dump the contents of the transition tables in a
|
|
-- format that shows the attribute order, so that we can distinguish
|
|
-- tuple formats (though not dropped attributes).
|
|
--
|
|
create or replace function dump_insert() returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
raise notice 'trigger = %, new table = %',
|
|
TG_NAME,
|
|
(select string_agg(new_table::text, ', ' order by a) from new_table);
|
|
return null;
|
|
end;
|
|
$$;
|
|
create or replace function dump_update() returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
raise notice 'trigger = %, old table = %, new table = %',
|
|
TG_NAME,
|
|
(select string_agg(old_table::text, ', ' order by a) from old_table),
|
|
(select string_agg(new_table::text, ', ' order by a) from new_table);
|
|
return null;
|
|
end;
|
|
$$;
|
|
create or replace function dump_delete() returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
raise notice 'trigger = %, old table = %',
|
|
TG_NAME,
|
|
(select string_agg(old_table::text, ', ' order by a) from old_table);
|
|
return null;
|
|
end;
|
|
$$;
|
|
--
|
|
-- Verify behavior of statement triggers on partition hierarchy with
|
|
-- transition tables. Tuples should appear to each trigger in the
|
|
-- format of the relation the trigger is attached to.
|
|
--
|
|
-- set up a partition hierarchy with some different TupleDescriptors
|
|
create table parent (a text, b int) partition by list (a);
|
|
-- a child matching parent
|
|
create table child1 partition of parent for values in ('AAA');
|
|
-- a child with a dropped column
|
|
create table child2 (x int, a text, b int);
|
|
alter table child2 drop column x;
|
|
alter table parent attach partition child2 for values in ('BBB');
|
|
-- a child with a different column order
|
|
create table child3 (b int, a text);
|
|
alter table parent attach partition child3 for values in ('CCC');
|
|
create trigger parent_insert_trig
|
|
after insert on parent referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger parent_update_trig
|
|
after update on parent referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger parent_delete_trig
|
|
after delete on parent referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
create trigger child1_insert_trig
|
|
after insert on child1 referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger child1_update_trig
|
|
after update on child1 referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger child1_delete_trig
|
|
after delete on child1 referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
create trigger child2_insert_trig
|
|
after insert on child2 referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger child2_update_trig
|
|
after update on child2 referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger child2_delete_trig
|
|
after delete on child2 referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
create trigger child3_insert_trig
|
|
after insert on child3 referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger child3_update_trig
|
|
after update on child3 referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger child3_delete_trig
|
|
after delete on child3 referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
SELECT trigger_name, event_manipulation, event_object_schema, event_object_table,
|
|
action_order, action_condition, action_orientation, action_timing,
|
|
action_reference_old_table, action_reference_new_table
|
|
FROM information_schema.triggers
|
|
WHERE event_object_table IN ('parent', 'child1', 'child2', 'child3')
|
|
ORDER BY trigger_name COLLATE "C", 2;
|
|
trigger_name | event_manipulation | event_object_schema | event_object_table | action_order | action_condition | action_orientation | action_timing | action_reference_old_table | action_reference_new_table
|
|
--------------------+--------------------+---------------------+--------------------+--------------+------------------+--------------------+---------------+----------------------------+----------------------------
|
|
child1_delete_trig | DELETE | public | child1 | 1 | | STATEMENT | AFTER | old_table |
|
|
child1_insert_trig | INSERT | public | child1 | 1 | | STATEMENT | AFTER | | new_table
|
|
child1_update_trig | UPDATE | public | child1 | 1 | | STATEMENT | AFTER | old_table | new_table
|
|
child2_delete_trig | DELETE | public | child2 | 1 | | STATEMENT | AFTER | old_table |
|
|
child2_insert_trig | INSERT | public | child2 | 1 | | STATEMENT | AFTER | | new_table
|
|
child2_update_trig | UPDATE | public | child2 | 1 | | STATEMENT | AFTER | old_table | new_table
|
|
child3_delete_trig | DELETE | public | child3 | 1 | | STATEMENT | AFTER | old_table |
|
|
child3_insert_trig | INSERT | public | child3 | 1 | | STATEMENT | AFTER | | new_table
|
|
child3_update_trig | UPDATE | public | child3 | 1 | | STATEMENT | AFTER | old_table | new_table
|
|
parent_delete_trig | DELETE | public | parent | 1 | | STATEMENT | AFTER | old_table |
|
|
parent_insert_trig | INSERT | public | parent | 1 | | STATEMENT | AFTER | | new_table
|
|
parent_update_trig | UPDATE | public | parent | 1 | | STATEMENT | AFTER | old_table | new_table
|
|
(12 rows)
|
|
|
|
-- insert directly into children sees respective child-format tuples
|
|
insert into child1 values ('AAA', 42);
|
|
NOTICE: trigger = child1_insert_trig, new table = (AAA,42)
|
|
insert into child2 values ('BBB', 42);
|
|
NOTICE: trigger = child2_insert_trig, new table = (BBB,42)
|
|
insert into child3 values (42, 'CCC');
|
|
NOTICE: trigger = child3_insert_trig, new table = (42,CCC)
|
|
-- update via parent sees parent-format tuples
|
|
update parent set b = b + 1;
|
|
NOTICE: trigger = parent_update_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = (AAA,43), (BBB,43), (CCC,43)
|
|
-- delete via parent sees parent-format tuples
|
|
delete from parent;
|
|
NOTICE: trigger = parent_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
|
|
-- insert into parent sees parent-format tuples
|
|
insert into parent values ('AAA', 42);
|
|
NOTICE: trigger = parent_insert_trig, new table = (AAA,42)
|
|
insert into parent values ('BBB', 42);
|
|
NOTICE: trigger = parent_insert_trig, new table = (BBB,42)
|
|
insert into parent values ('CCC', 42);
|
|
NOTICE: trigger = parent_insert_trig, new table = (CCC,42)
|
|
-- delete from children sees respective child-format tuples
|
|
delete from child1;
|
|
NOTICE: trigger = child1_delete_trig, old table = (AAA,42)
|
|
delete from child2;
|
|
NOTICE: trigger = child2_delete_trig, old table = (BBB,42)
|
|
delete from child3;
|
|
NOTICE: trigger = child3_delete_trig, old table = (42,CCC)
|
|
-- copy into parent sees parent-format tuples
|
|
copy parent (a, b) from stdin;
|
|
NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42)
|
|
-- DML affecting parent sees tuples collected from children even if
|
|
-- there is no transition table trigger on the children
|
|
drop trigger child1_insert_trig on child1;
|
|
drop trigger child1_update_trig on child1;
|
|
drop trigger child1_delete_trig on child1;
|
|
drop trigger child2_insert_trig on child2;
|
|
drop trigger child2_update_trig on child2;
|
|
drop trigger child2_delete_trig on child2;
|
|
drop trigger child3_insert_trig on child3;
|
|
drop trigger child3_update_trig on child3;
|
|
drop trigger child3_delete_trig on child3;
|
|
delete from parent;
|
|
NOTICE: trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42)
|
|
-- copy into parent sees tuples collected from children even if there
|
|
-- is no transition-table trigger on the children
|
|
copy parent (a, b) from stdin;
|
|
NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42)
|
|
-- insert into parent with a before trigger on a child tuple before
|
|
-- insertion, and we capture the newly modified row in parent format
|
|
create or replace function intercept_insert() returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
new.b = new.b + 1000;
|
|
return new;
|
|
end;
|
|
$$;
|
|
create trigger intercept_insert_child3
|
|
before insert on child3
|
|
for each row execute procedure intercept_insert();
|
|
-- insert, parent trigger sees post-modification parent-format tuple
|
|
insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
|
|
NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1066)
|
|
-- copy, parent trigger sees post-modification parent-format tuple
|
|
copy parent (a, b) from stdin;
|
|
NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1234)
|
|
drop table child1, child2, child3, parent;
|
|
drop function intercept_insert();
|
|
--
|
|
-- Verify prohibition of row triggers with transition triggers on
|
|
-- partitions
|
|
--
|
|
create table parent (a text, b int) partition by list (a);
|
|
create table child partition of parent for values in ('AAA');
|
|
-- adding row trigger with transition table fails
|
|
create trigger child_row_trig
|
|
after insert on child referencing new table as new_table
|
|
for each row execute procedure dump_insert();
|
|
ERROR: ROW triggers with transition tables are not supported on partitions
|
|
-- detaching it first works
|
|
alter table parent detach partition child;
|
|
create trigger child_row_trig
|
|
after insert on child referencing new table as new_table
|
|
for each row execute procedure dump_insert();
|
|
-- but now we're not allowed to reattach it
|
|
alter table parent attach partition child for values in ('AAA');
|
|
ERROR: trigger "child_row_trig" prevents table "child" from becoming a partition
|
|
DETAIL: ROW triggers with transition tables are not supported on partitions.
|
|
-- drop the trigger, and now we're allowed to attach it again
|
|
drop trigger child_row_trig on child;
|
|
alter table parent attach partition child for values in ('AAA');
|
|
drop table child, parent;
|
|
--
|
|
-- Verify access of transition tables with UPDATE triggers and tuples
|
|
-- moved across partitions.
|
|
--
|
|
create or replace function dump_update_new() returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
raise notice 'trigger = %, new table = %', TG_NAME,
|
|
(select string_agg(new_table::text, ', ' order by a) from new_table);
|
|
return null;
|
|
end;
|
|
$$;
|
|
create or replace function dump_update_old() returns trigger language plpgsql as
|
|
$$
|
|
begin
|
|
raise notice 'trigger = %, old table = %', TG_NAME,
|
|
(select string_agg(old_table::text, ', ' order by a) from old_table);
|
|
return null;
|
|
end;
|
|
$$;
|
|
create table trans_tab_parent (a text) partition by list (a);
|
|
create table trans_tab_child1 partition of trans_tab_parent for values in ('AAA1', 'AAA2');
|
|
create table trans_tab_child2 partition of trans_tab_parent for values in ('BBB1', 'BBB2');
|
|
create trigger trans_tab_parent_update_trig
|
|
after update on trans_tab_parent referencing old table as old_table
|
|
for each statement execute procedure dump_update_old();
|
|
create trigger trans_tab_parent_insert_trig
|
|
after insert on trans_tab_parent referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger trans_tab_parent_delete_trig
|
|
after delete on trans_tab_parent referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
insert into trans_tab_parent values ('AAA1'), ('BBB1');
|
|
NOTICE: trigger = trans_tab_parent_insert_trig, new table = (AAA1), (BBB1)
|
|
-- should not trigger access to new table when moving across partitions.
|
|
update trans_tab_parent set a = 'BBB2' where a = 'AAA1';
|
|
NOTICE: trigger = trans_tab_parent_update_trig, old table = (AAA1)
|
|
drop trigger trans_tab_parent_update_trig on trans_tab_parent;
|
|
create trigger trans_tab_parent_update_trig
|
|
after update on trans_tab_parent referencing new table as new_table
|
|
for each statement execute procedure dump_update_new();
|
|
-- should not trigger access to old table when moving across partitions.
|
|
update trans_tab_parent set a = 'AAA2' where a = 'BBB1';
|
|
NOTICE: trigger = trans_tab_parent_update_trig, new table = (AAA2)
|
|
delete from trans_tab_parent;
|
|
NOTICE: trigger = trans_tab_parent_delete_trig, old table = (AAA2), (BBB2)
|
|
-- clean up
|
|
drop table trans_tab_parent, trans_tab_child1, trans_tab_child2;
|
|
drop function dump_update_new, dump_update_old;
|
|
--
|
|
-- Verify behavior of statement triggers on (non-partition)
|
|
-- inheritance hierarchy with transition tables; similar to the
|
|
-- partition case, except there is no rerouting on insertion and child
|
|
-- tables can have extra columns
|
|
--
|
|
-- set up inheritance hierarchy with different TupleDescriptors
|
|
create table parent (a text, b int);
|
|
-- a child matching parent
|
|
create table child1 () inherits (parent);
|
|
-- a child with a different column order
|
|
create table child2 (b int, a text);
|
|
alter table child2 inherit parent;
|
|
-- a child with an extra column
|
|
create table child3 (c text) inherits (parent);
|
|
create trigger parent_insert_trig
|
|
after insert on parent referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger parent_update_trig
|
|
after update on parent referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger parent_delete_trig
|
|
after delete on parent referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
create trigger child1_insert_trig
|
|
after insert on child1 referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger child1_update_trig
|
|
after update on child1 referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger child1_delete_trig
|
|
after delete on child1 referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
create trigger child2_insert_trig
|
|
after insert on child2 referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger child2_update_trig
|
|
after update on child2 referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger child2_delete_trig
|
|
after delete on child2 referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
create trigger child3_insert_trig
|
|
after insert on child3 referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger child3_update_trig
|
|
after update on child3 referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger child3_delete_trig
|
|
after delete on child3 referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
-- insert directly into children sees respective child-format tuples
|
|
insert into child1 values ('AAA', 42);
|
|
NOTICE: trigger = child1_insert_trig, new table = (AAA,42)
|
|
insert into child2 values (42, 'BBB');
|
|
NOTICE: trigger = child2_insert_trig, new table = (42,BBB)
|
|
insert into child3 values ('CCC', 42, 'foo');
|
|
NOTICE: trigger = child3_insert_trig, new table = (CCC,42,foo)
|
|
-- update via parent sees parent-format tuples
|
|
update parent set b = b + 1;
|
|
NOTICE: trigger = parent_update_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = (AAA,43), (BBB,43), (CCC,43)
|
|
-- delete via parent sees parent-format tuples
|
|
delete from parent;
|
|
NOTICE: trigger = parent_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
|
|
-- reinsert values into children for next test...
|
|
insert into child1 values ('AAA', 42);
|
|
NOTICE: trigger = child1_insert_trig, new table = (AAA,42)
|
|
insert into child2 values (42, 'BBB');
|
|
NOTICE: trigger = child2_insert_trig, new table = (42,BBB)
|
|
insert into child3 values ('CCC', 42, 'foo');
|
|
NOTICE: trigger = child3_insert_trig, new table = (CCC,42,foo)
|
|
-- delete from children sees respective child-format tuples
|
|
delete from child1;
|
|
NOTICE: trigger = child1_delete_trig, old table = (AAA,42)
|
|
delete from child2;
|
|
NOTICE: trigger = child2_delete_trig, old table = (42,BBB)
|
|
delete from child3;
|
|
NOTICE: trigger = child3_delete_trig, old table = (CCC,42,foo)
|
|
-- copy into parent sees parent-format tuples (no rerouting, so these
|
|
-- are really inserted into the parent)
|
|
copy parent (a, b) from stdin;
|
|
NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42)
|
|
-- same behavior for copy if there is an index (interesting because rows are
|
|
-- captured by a different code path in copyfrom.c if there are indexes)
|
|
create index on parent(b);
|
|
copy parent (a, b) from stdin;
|
|
NOTICE: trigger = parent_insert_trig, new table = (DDD,42)
|
|
-- DML affecting parent sees tuples collected from children even if
|
|
-- there is no transition table trigger on the children
|
|
drop trigger child1_insert_trig on child1;
|
|
drop trigger child1_update_trig on child1;
|
|
drop trigger child1_delete_trig on child1;
|
|
drop trigger child2_insert_trig on child2;
|
|
drop trigger child2_update_trig on child2;
|
|
drop trigger child2_delete_trig on child2;
|
|
drop trigger child3_insert_trig on child3;
|
|
drop trigger child3_update_trig on child3;
|
|
drop trigger child3_delete_trig on child3;
|
|
delete from parent;
|
|
NOTICE: trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42), (DDD,42)
|
|
drop table child1, child2, child3, parent;
|
|
--
|
|
-- Verify prohibition of row triggers with transition triggers on
|
|
-- inheritance children
|
|
--
|
|
create table parent (a text, b int);
|
|
create table child () inherits (parent);
|
|
-- adding row trigger with transition table fails
|
|
create trigger child_row_trig
|
|
after insert on child referencing new table as new_table
|
|
for each row execute procedure dump_insert();
|
|
ERROR: ROW triggers with transition tables are not supported on inheritance children
|
|
-- disinheriting it first works
|
|
alter table child no inherit parent;
|
|
create trigger child_row_trig
|
|
after insert on child referencing new table as new_table
|
|
for each row execute procedure dump_insert();
|
|
-- but now we're not allowed to make it inherit anymore
|
|
alter table child inherit parent;
|
|
ERROR: trigger "child_row_trig" prevents table "child" from becoming an inheritance child
|
|
DETAIL: ROW triggers with transition tables are not supported in inheritance hierarchies.
|
|
-- drop the trigger, and now we're allowed to make it inherit again
|
|
drop trigger child_row_trig on child;
|
|
alter table child inherit parent;
|
|
drop table child, parent;
|
|
--
|
|
-- Verify behavior of queries with wCTEs, where multiple transition
|
|
-- tuplestores can be active at the same time because there are
|
|
-- multiple DML statements that might fire triggers with transition
|
|
-- tables
|
|
--
|
|
create table table1 (a int);
|
|
create table table2 (a text);
|
|
create trigger table1_trig
|
|
after insert on table1 referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger table2_trig
|
|
after insert on table2 referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
with wcte as (insert into table1 values (42))
|
|
insert into table2 values ('hello world');
|
|
NOTICE: trigger = table2_trig, new table = ("hello world")
|
|
NOTICE: trigger = table1_trig, new table = (42)
|
|
with wcte as (insert into table1 values (43))
|
|
insert into table1 values (44);
|
|
NOTICE: trigger = table1_trig, new table = (43), (44)
|
|
select * from table1;
|
|
a
|
|
----
|
|
42
|
|
44
|
|
43
|
|
(3 rows)
|
|
|
|
select * from table2;
|
|
a
|
|
-------------
|
|
hello world
|
|
(1 row)
|
|
|
|
drop table table1;
|
|
drop table table2;
|
|
--
|
|
-- Verify behavior of INSERT ... ON CONFLICT DO UPDATE ... with
|
|
-- transition tables.
|
|
--
|
|
create table my_table (a int primary key, b text);
|
|
create trigger my_table_insert_trig
|
|
after insert on my_table referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger my_table_update_trig
|
|
after update on my_table referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
-- inserts only
|
|
insert into my_table values (1, 'AAA'), (2, 'BBB')
|
|
on conflict (a) do
|
|
update set b = my_table.b || ':' || excluded.b;
|
|
NOTICE: trigger = my_table_update_trig, old table = <NULL>, new table = <NULL>
|
|
NOTICE: trigger = my_table_insert_trig, new table = (1,AAA), (2,BBB)
|
|
-- mixture of inserts and updates
|
|
insert into my_table values (1, 'AAA'), (2, 'BBB'), (3, 'CCC'), (4, 'DDD')
|
|
on conflict (a) do
|
|
update set b = my_table.b || ':' || excluded.b;
|
|
NOTICE: trigger = my_table_update_trig, old table = (1,AAA), (2,BBB), new table = (1,AAA:AAA), (2,BBB:BBB)
|
|
NOTICE: trigger = my_table_insert_trig, new table = (3,CCC), (4,DDD)
|
|
-- updates only
|
|
insert into my_table values (3, 'CCC'), (4, 'DDD')
|
|
on conflict (a) do
|
|
update set b = my_table.b || ':' || excluded.b;
|
|
NOTICE: trigger = my_table_update_trig, old table = (3,CCC), (4,DDD), new table = (3,CCC:CCC), (4,DDD:DDD)
|
|
NOTICE: trigger = my_table_insert_trig, new table = <NULL>
|
|
--
|
|
-- now using a partitioned table
|
|
--
|
|
create table iocdu_tt_parted (a int primary key, b text) partition by list (a);
|
|
create table iocdu_tt_parted1 partition of iocdu_tt_parted for values in (1);
|
|
create table iocdu_tt_parted2 partition of iocdu_tt_parted for values in (2);
|
|
create table iocdu_tt_parted3 partition of iocdu_tt_parted for values in (3);
|
|
create table iocdu_tt_parted4 partition of iocdu_tt_parted for values in (4);
|
|
create trigger iocdu_tt_parted_insert_trig
|
|
after insert on iocdu_tt_parted referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger iocdu_tt_parted_update_trig
|
|
after update on iocdu_tt_parted referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
-- inserts only
|
|
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
|
|
on conflict (a) do
|
|
update set b = iocdu_tt_parted.b || ':' || excluded.b;
|
|
NOTICE: trigger = iocdu_tt_parted_update_trig, old table = <NULL>, new table = <NULL>
|
|
NOTICE: trigger = iocdu_tt_parted_insert_trig, new table = (1,AAA), (2,BBB)
|
|
-- mixture of inserts and updates
|
|
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB'), (3, 'CCC'), (4, 'DDD')
|
|
on conflict (a) do
|
|
update set b = iocdu_tt_parted.b || ':' || excluded.b;
|
|
NOTICE: trigger = iocdu_tt_parted_update_trig, old table = (1,AAA), (2,BBB), new table = (1,AAA:AAA), (2,BBB:BBB)
|
|
NOTICE: trigger = iocdu_tt_parted_insert_trig, new table = (3,CCC), (4,DDD)
|
|
-- updates only
|
|
insert into iocdu_tt_parted values (3, 'CCC'), (4, 'DDD')
|
|
on conflict (a) do
|
|
update set b = iocdu_tt_parted.b || ':' || excluded.b;
|
|
NOTICE: trigger = iocdu_tt_parted_update_trig, old table = (3,CCC), (4,DDD), new table = (3,CCC:CCC), (4,DDD:DDD)
|
|
NOTICE: trigger = iocdu_tt_parted_insert_trig, new table = <NULL>
|
|
drop table iocdu_tt_parted;
|
|
--
|
|
-- Verify that you can't create a trigger with transition tables for
|
|
-- more than one event.
|
|
--
|
|
create trigger my_table_multievent_trig
|
|
after insert or update on my_table referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
ERROR: transition tables cannot be specified for triggers with more than one event
|
|
--
|
|
-- Verify that you can't create a trigger with transition tables with
|
|
-- a column list.
|
|
--
|
|
create trigger my_table_col_update_trig
|
|
after update of b on my_table referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
ERROR: transition tables cannot be specified for triggers with column lists
|
|
drop table my_table;
|
|
--
|
|
-- Test firing of triggers with transition tables by foreign key cascades
|
|
--
|
|
create table refd_table (a int primary key, b text);
|
|
create table trig_table (a int, b text,
|
|
foreign key (a) references refd_table on update cascade on delete cascade
|
|
);
|
|
create trigger trig_table_before_trig
|
|
before insert or update or delete on trig_table
|
|
for each statement execute procedure trigger_func('trig_table');
|
|
create trigger trig_table_insert_trig
|
|
after insert on trig_table referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger trig_table_update_trig
|
|
after update on trig_table referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger trig_table_delete_trig
|
|
after delete on trig_table referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
insert into refd_table values
|
|
(1, 'one'),
|
|
(2, 'two'),
|
|
(3, 'three');
|
|
insert into trig_table values
|
|
(1, 'one a'),
|
|
(1, 'one b'),
|
|
(2, 'two a'),
|
|
(2, 'two b'),
|
|
(3, 'three a'),
|
|
(3, 'three b');
|
|
NOTICE: trigger_func(trig_table) called: action = INSERT, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger = trig_table_insert_trig, new table = (1,"one a"), (1,"one b"), (2,"two a"), (2,"two b"), (3,"three a"), (3,"three b")
|
|
update refd_table set a = 11 where b = 'one';
|
|
NOTICE: trigger_func(trig_table) called: action = UPDATE, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger = trig_table_update_trig, old table = (1,"one a"), (1,"one b"), new table = (11,"one a"), (11,"one b")
|
|
select * from trig_table;
|
|
a | b
|
|
----+---------
|
|
2 | two a
|
|
2 | two b
|
|
3 | three a
|
|
3 | three b
|
|
11 | one a
|
|
11 | one b
|
|
(6 rows)
|
|
|
|
delete from refd_table where length(b) = 3;
|
|
NOTICE: trigger_func(trig_table) called: action = DELETE, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger = trig_table_delete_trig, old table = (2,"two a"), (2,"two b"), (11,"one a"), (11,"one b")
|
|
select * from trig_table;
|
|
a | b
|
|
---+---------
|
|
3 | three a
|
|
3 | three b
|
|
(2 rows)
|
|
|
|
drop table refd_table, trig_table;
|
|
--
|
|
-- Test that we can drop a not-yet-fired deferred trigger
|
|
--
|
|
create table refd_table (id int primary key);
|
|
create table trig_table (fk int references refd_table initially deferred);
|
|
begin;
|
|
insert into trig_table values (1);
|
|
drop table refd_table cascade;
|
|
NOTICE: drop cascades to constraint trig_table_fk_fkey on table trig_table
|
|
commit;
|
|
drop table trig_table;
|
|
--
|
|
-- self-referential FKs are even more fun
|
|
--
|
|
create table self_ref (a int primary key,
|
|
b int references self_ref(a) on delete cascade);
|
|
create trigger self_ref_before_trig
|
|
before delete on self_ref
|
|
for each statement execute procedure trigger_func('self_ref');
|
|
create trigger self_ref_r_trig
|
|
after delete on self_ref referencing old table as old_table
|
|
for each row execute procedure dump_delete();
|
|
create trigger self_ref_s_trig
|
|
after delete on self_ref referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
insert into self_ref values (1, null), (2, 1), (3, 2);
|
|
delete from self_ref where a = 1;
|
|
NOTICE: trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger = self_ref_r_trig, old table = (1,), (2,1)
|
|
NOTICE: trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger = self_ref_r_trig, old table = (1,), (2,1)
|
|
NOTICE: trigger = self_ref_s_trig, old table = (1,), (2,1)
|
|
NOTICE: trigger = self_ref_r_trig, old table = (3,2)
|
|
NOTICE: trigger = self_ref_s_trig, old table = (3,2)
|
|
-- without AR trigger, cascaded deletes all end up in one transition table
|
|
drop trigger self_ref_r_trig on self_ref;
|
|
insert into self_ref values (1, null), (2, 1), (3, 2), (4, 3);
|
|
delete from self_ref where a = 1;
|
|
NOTICE: trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT
|
|
NOTICE: trigger = self_ref_s_trig, old table = (1,), (2,1), (3,2), (4,3)
|
|
drop table self_ref;
|
|
--
|
|
-- test transition tables with MERGE
|
|
--
|
|
create table merge_target_table (a int primary key, b text);
|
|
create trigger merge_target_table_insert_trig
|
|
after insert on merge_target_table referencing new table as new_table
|
|
for each statement execute procedure dump_insert();
|
|
create trigger merge_target_table_update_trig
|
|
after update on merge_target_table referencing old table as old_table new table as new_table
|
|
for each statement execute procedure dump_update();
|
|
create trigger merge_target_table_delete_trig
|
|
after delete on merge_target_table referencing old table as old_table
|
|
for each statement execute procedure dump_delete();
|
|
create table merge_source_table (a int, b text);
|
|
insert into merge_source_table
|
|
values (1, 'initial1'), (2, 'initial2'),
|
|
(3, 'initial3'), (4, 'initial4');
|
|
merge into merge_target_table t
|
|
using merge_source_table s
|
|
on t.a = s.a
|
|
when not matched then
|
|
insert values (a, b);
|
|
NOTICE: trigger = merge_target_table_insert_trig, new table = (1,initial1), (2,initial2), (3,initial3), (4,initial4)
|
|
merge into merge_target_table t
|
|
using merge_source_table s
|
|
on t.a = s.a
|
|
when matched and s.a <= 2 then
|
|
update set b = t.b || ' updated by merge'
|
|
when matched and s.a > 2 then
|
|
delete
|
|
when not matched then
|
|
insert values (a, b);
|
|
NOTICE: trigger = merge_target_table_delete_trig, old table = (3,initial3), (4,initial4)
|
|
NOTICE: trigger = merge_target_table_update_trig, old table = (1,initial1), (2,initial2), new table = (1,"initial1 updated by merge"), (2,"initial2 updated by merge")
|
|
NOTICE: trigger = merge_target_table_insert_trig, new table = <NULL>
|
|
merge into merge_target_table t
|
|
using merge_source_table s
|
|
on t.a = s.a
|
|
when matched and s.a <= 2 then
|
|
update set b = t.b || ' updated again by merge'
|
|
when matched and s.a > 2 then
|
|
delete
|
|
when not matched then
|
|
insert values (a, b);
|
|
NOTICE: trigger = merge_target_table_delete_trig, old table = <NULL>
|
|
NOTICE: trigger = merge_target_table_update_trig, old table = (1,"initial1 updated by merge"), (2,"initial2 updated by merge"), new table = (1,"initial1 updated by merge updated again by merge"), (2,"initial2 updated by merge updated again by merge")
|
|
NOTICE: trigger = merge_target_table_insert_trig, new table = (3,initial3), (4,initial4)
|
|
drop table merge_source_table, merge_target_table;
|
|
-- cleanup
|
|
drop function dump_insert();
|
|
drop function dump_update();
|
|
drop function dump_delete();
|
|
--
|
|
-- Tests for CREATE OR REPLACE TRIGGER
|
|
--
|
|
create table my_table (id integer);
|
|
create function funcA() returns trigger as $$
|
|
begin
|
|
raise notice 'hello from funcA';
|
|
return null;
|
|
end; $$ language plpgsql;
|
|
create function funcB() returns trigger as $$
|
|
begin
|
|
raise notice 'hello from funcB';
|
|
return null;
|
|
end; $$ language plpgsql;
|
|
create trigger my_trig
|
|
after insert on my_table
|
|
for each row execute procedure funcA();
|
|
create trigger my_trig
|
|
before insert on my_table
|
|
for each row execute procedure funcB(); -- should fail
|
|
ERROR: trigger "my_trig" for relation "my_table" already exists
|
|
insert into my_table values (1);
|
|
NOTICE: hello from funcA
|
|
create or replace trigger my_trig
|
|
before insert on my_table
|
|
for each row execute procedure funcB(); -- OK
|
|
insert into my_table values (2); -- this insert should become a no-op
|
|
NOTICE: hello from funcB
|
|
table my_table;
|
|
id
|
|
----
|
|
1
|
|
(1 row)
|
|
|
|
drop table my_table;
|
|
-- test CREATE OR REPLACE TRIGGER on partition table
|
|
create table parted_trig (a int) partition by range (a);
|
|
create table parted_trig_1 partition of parted_trig
|
|
for values from (0) to (1000) partition by range (a);
|
|
create table parted_trig_1_1 partition of parted_trig_1 for values from (0) to (100);
|
|
create table parted_trig_2 partition of parted_trig for values from (1000) to (2000);
|
|
create table default_parted_trig partition of parted_trig default;
|
|
-- test that trigger can be replaced by another one
|
|
-- at the same level of partition table
|
|
create or replace trigger my_trig
|
|
after insert on parted_trig
|
|
for each row execute procedure funcA();
|
|
insert into parted_trig (a) values (50);
|
|
NOTICE: hello from funcA
|
|
create or replace trigger my_trig
|
|
after insert on parted_trig
|
|
for each row execute procedure funcB();
|
|
insert into parted_trig (a) values (50);
|
|
NOTICE: hello from funcB
|
|
-- test that child trigger cannot be replaced directly
|
|
create or replace trigger my_trig
|
|
after insert on parted_trig
|
|
for each row execute procedure funcA();
|
|
insert into parted_trig (a) values (50);
|
|
NOTICE: hello from funcA
|
|
create or replace trigger my_trig
|
|
after insert on parted_trig_1
|
|
for each row execute procedure funcB(); -- should fail
|
|
ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
|
|
insert into parted_trig (a) values (50);
|
|
NOTICE: hello from funcA
|
|
drop trigger my_trig on parted_trig;
|
|
insert into parted_trig (a) values (50);
|
|
-- test that user trigger can be overwritten by one defined at upper level
|
|
create trigger my_trig
|
|
after insert on parted_trig_1
|
|
for each row execute procedure funcA();
|
|
insert into parted_trig (a) values (50);
|
|
NOTICE: hello from funcA
|
|
create trigger my_trig
|
|
after insert on parted_trig
|
|
for each row execute procedure funcB(); -- should fail
|
|
ERROR: trigger "my_trig" for relation "parted_trig_1" already exists
|
|
insert into parted_trig (a) values (50);
|
|
NOTICE: hello from funcA
|
|
create or replace trigger my_trig
|
|
after insert on parted_trig
|
|
for each row execute procedure funcB();
|
|
insert into parted_trig (a) values (50);
|
|
NOTICE: hello from funcB
|
|
-- cleanup
|
|
drop table parted_trig;
|
|
drop function funcA();
|
|
drop function funcB();
|
|
-- Leave around some objects for other tests
|
|
create table trigger_parted (a int primary key) partition by list (a);
|
|
create function trigger_parted_trigfunc() returns trigger language plpgsql as
|
|
$$ begin end; $$;
|
|
create trigger aft_row after insert or update on trigger_parted
|
|
for each row execute function trigger_parted_trigfunc();
|
|
create table trigger_parted_p1 partition of trigger_parted for values in (1)
|
|
partition by list (a);
|
|
create table trigger_parted_p1_1 partition of trigger_parted_p1 for values in (1);
|
|
create table trigger_parted_p2 partition of trigger_parted for values in (2)
|
|
partition by list (a);
|
|
create table trigger_parted_p2_2 partition of trigger_parted_p2 for values in (2);
|
|
alter table only trigger_parted_p2 disable trigger aft_row;
|
|
alter table trigger_parted_p2_2 enable always trigger aft_row;
|
|
-- verify transition table conversion slot's lifetime
|
|
-- https://postgr.es/m/39a71864-b120-5a5c-8cc5-c632b6f16761@amazon.com
|
|
create table convslot_test_parent (col1 text primary key);
|
|
create table convslot_test_child (col1 text primary key,
|
|
foreign key (col1) references convslot_test_parent(col1) on delete cascade on update cascade
|
|
);
|
|
alter table convslot_test_child add column col2 text not null default 'tutu';
|
|
insert into convslot_test_parent(col1) values ('1');
|
|
insert into convslot_test_child(col1) values ('1');
|
|
insert into convslot_test_parent(col1) values ('3');
|
|
insert into convslot_test_child(col1) values ('3');
|
|
create function convslot_trig1()
|
|
returns trigger
|
|
language plpgsql
|
|
AS $$
|
|
begin
|
|
raise notice 'trigger = %, old_table = %',
|
|
TG_NAME,
|
|
(select string_agg(old_table::text, ', ' order by col1) from old_table);
|
|
return null;
|
|
end; $$;
|
|
create function convslot_trig2()
|
|
returns trigger
|
|
language plpgsql
|
|
AS $$
|
|
begin
|
|
raise notice 'trigger = %, new table = %',
|
|
TG_NAME,
|
|
(select string_agg(new_table::text, ', ' order by col1) from new_table);
|
|
return null;
|
|
end; $$;
|
|
create trigger but_trigger after update on convslot_test_child
|
|
referencing new table as new_table
|
|
for each statement execute function convslot_trig2();
|
|
update convslot_test_parent set col1 = col1 || '1';
|
|
NOTICE: trigger = but_trigger, new table = (11,tutu), (31,tutu)
|
|
create function convslot_trig3()
|
|
returns trigger
|
|
language plpgsql
|
|
AS $$
|
|
begin
|
|
raise notice 'trigger = %, old_table = %, new table = %',
|
|
TG_NAME,
|
|
(select string_agg(old_table::text, ', ' order by col1) from old_table),
|
|
(select string_agg(new_table::text, ', ' order by col1) from new_table);
|
|
return null;
|
|
end; $$;
|
|
create trigger but_trigger2 after update on convslot_test_child
|
|
referencing old table as old_table new table as new_table
|
|
for each statement execute function convslot_trig3();
|
|
update convslot_test_parent set col1 = col1 || '1';
|
|
NOTICE: trigger = but_trigger, new table = (111,tutu), (311,tutu)
|
|
NOTICE: trigger = but_trigger2, old_table = (11,tutu), (31,tutu), new table = (111,tutu), (311,tutu)
|
|
create trigger bdt_trigger after delete on convslot_test_child
|
|
referencing old table as old_table
|
|
for each statement execute function convslot_trig1();
|
|
delete from convslot_test_parent;
|
|
NOTICE: trigger = bdt_trigger, old_table = (111,tutu), (311,tutu)
|
|
drop table convslot_test_child, convslot_test_parent;
|
|
drop function convslot_trig1();
|
|
drop function convslot_trig2();
|
|
drop function convslot_trig3();
|
|
-- Bug #17607: variant of above in which trigger function raises an error;
|
|
-- we don't see any ill effects unless trigger tuple requires mapping
|
|
create table convslot_test_parent (id int primary key, val int)
|
|
partition by range (id);
|
|
create table convslot_test_part (val int, id int not null);
|
|
alter table convslot_test_parent
|
|
attach partition convslot_test_part for values from (1) to (1000);
|
|
create function convslot_trig4() returns trigger as
|
|
$$begin raise exception 'BOOM!'; end$$ language plpgsql;
|
|
create trigger convslot_test_parent_update
|
|
after update on convslot_test_parent
|
|
referencing old table as old_rows new table as new_rows
|
|
for each statement execute procedure convslot_trig4();
|
|
insert into convslot_test_parent (id, val) values (1, 2);
|
|
begin;
|
|
savepoint svp;
|
|
update convslot_test_parent set val = 3; -- error expected
|
|
ERROR: BOOM!
|
|
CONTEXT: PL/pgSQL function convslot_trig4() line 1 at RAISE
|
|
rollback to savepoint svp;
|
|
rollback;
|
|
drop table convslot_test_parent;
|
|
drop function convslot_trig4();
|
|
-- Test trigger renaming on partitioned tables
|
|
create table grandparent (id int, primary key (id)) partition by range (id);
|
|
create table middle partition of grandparent for values from (1) to (10)
|
|
partition by range (id);
|
|
create table chi partition of middle for values from (1) to (5);
|
|
create table cho partition of middle for values from (6) to (10);
|
|
create function f () returns trigger as
|
|
$$ begin return new; end; $$
|
|
language plpgsql;
|
|
create trigger a after insert on grandparent
|
|
for each row execute procedure f();
|
|
alter trigger a on grandparent rename to b;
|
|
select tgrelid::regclass, tgname,
|
|
(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
|
|
from pg_trigger where tgrelid in (select relid from pg_partition_tree('grandparent'))
|
|
order by tgname, tgrelid::regclass::text COLLATE "C";
|
|
tgrelid | tgname | parent_tgname
|
|
-------------+--------+---------------
|
|
chi | b | b
|
|
cho | b | b
|
|
grandparent | b |
|
|
middle | b | b
|
|
(4 rows)
|
|
|
|
alter trigger a on only grandparent rename to b; -- ONLY not supported
|
|
ERROR: syntax error at or near "only"
|
|
LINE 1: alter trigger a on only grandparent rename to b;
|
|
^
|
|
alter trigger b on middle rename to c; -- can't rename trigger on partition
|
|
ERROR: cannot rename trigger "b" on table "middle"
|
|
HINT: Rename the trigger on the partitioned table "grandparent" instead.
|
|
create trigger c after insert on middle
|
|
for each row execute procedure f();
|
|
alter trigger b on grandparent rename to c;
|
|
ERROR: trigger "c" for relation "middle" already exists
|
|
-- Rename cascading does not affect statement triggers
|
|
create trigger p after insert on grandparent for each statement execute function f();
|
|
create trigger p after insert on middle for each statement execute function f();
|
|
alter trigger p on grandparent rename to q;
|
|
select tgrelid::regclass, tgname,
|
|
(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
|
|
from pg_trigger where tgrelid in (select relid from pg_partition_tree('grandparent'))
|
|
order by tgname, tgrelid::regclass::text COLLATE "C";
|
|
tgrelid | tgname | parent_tgname
|
|
-------------+--------+---------------
|
|
chi | b | b
|
|
cho | b | b
|
|
grandparent | b |
|
|
middle | b | b
|
|
chi | c | c
|
|
cho | c | c
|
|
middle | c |
|
|
middle | p |
|
|
grandparent | q |
|
|
(9 rows)
|
|
|
|
drop table grandparent;
|
|
-- Trigger renaming does not recurse on legacy inheritance
|
|
create table parent (a int);
|
|
create table child () inherits (parent);
|
|
create trigger parenttrig after insert on parent
|
|
for each row execute procedure f();
|
|
create trigger parenttrig after insert on child
|
|
for each row execute procedure f();
|
|
alter trigger parenttrig on parent rename to anothertrig;
|
|
\d+ child
|
|
Table "public.child"
|
|
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
|
|
--------+---------+-----------+----------+---------+---------+--------------+-------------
|
|
a | integer | | | | plain | |
|
|
Triggers:
|
|
parenttrig AFTER INSERT ON child FOR EACH ROW EXECUTE FUNCTION f()
|
|
Inherits: parent
|
|
|
|
drop table parent, child;
|
|
drop function f();
|