postgresql/src/test/regress/expected/triggers.out

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

3720 lines
161 KiB
Plaintext
Raw Normal View History

2000-01-05 18:31:08 +01:00
--
-- 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;
2000-01-05 18:31:08 +01:00
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
1997-09-11 11:14:12 +02:00
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');
2000-01-05 18:31:08 +01:00
--
-- For fkeys2:
-- (fkey21, fkey22) --> pkeys (pkey1, pkey2)
--
create trigger check_fkeys2_pkey_exist
before insert or update on fkeys2
for each row
execute procedure
1997-09-11 11:14:12 +02:00
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;
2000-01-05 18:31:08 +01:00
--
-- 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',
1997-09-11 11:14:12 +02:00
'fkeys', 'fkey1', 'fkey2', 'fkeys2', 'fkey21', 'fkey22');
2000-01-05 18:31:08 +01:00
--
-- For fkeys2:
-- ON DELETE/UPDATE (pkey23) RESTRICT:
-- fkeys (fkey3)
--
create trigger check_fkeys2_fkey_restrict
1997-09-11 11:14:12 +02:00
before delete or update on fkeys2
for each row
1997-09-11 11:14:12 +02:00
execute procedure check_foreign_key (1, 'restrict', 'pkey23', 'fkeys', 'fkey3');
2000-01-05 18:31:08 +01:00
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".
2000-01-05 18:31:08 +01:00
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".
2000-01-05 18:31:08 +01:00
-- 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".
2000-01-05 18:31:08 +01:00
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 "
2000-01-05 18:31:08 +01:00
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
2000-01-05 18:31:08 +01:00
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)
2000-01-05 18:31:08 +01:00
DROP TABLE pkeys;
DROP TABLE fkeys;
DROP TABLE fkeys2;
Prevent dangling-pointer access when update trigger returns old tuple. A before-update row trigger may choose to return the "new" or "old" tuple unmodified. ExecBRUpdateTriggers failed to consider the second possibility, and would proceed to free the "old" tuple even if it was the one returned, leading to subsequent access to already-deallocated memory. In debug builds this reliably leads to an "invalid memory alloc request size" failure; in production builds it might accidentally work, but data corruption is also possible. This is a very old bug. There are probably a couple of reasons it hasn't been noticed up to now. It would be more usual to return NULL if one wanted to suppress the update action; returning "old" is significantly less efficient since the update will occur anyway. Also, none of the standard PLs would ever cause this because they all returned freshly-manufactured tuples even if they were just copying "old". But commit 4b93f5799 changed that for plpgsql, making it possible to see the bug with a plpgsql trigger. Still, this is certainly legal behavior for a trigger function, so it's ExecBRUpdateTriggers's fault not plpgsql's. It seems worth creating a test case that exercises returning "old" directly with a C-language trigger; testing this through plpgsql seems unreliable because its behavior might change again. Report and fix by Rushabh Lathia; regression test case by me. Back-patch to all supported branches. Discussion: https://postgr.es/m/CAGPqQf1P4pjiNPrMof=P_16E-DFjt457j+nH2ex3=nBTew7tXw@mail.gmail.com
2018-02-27 19:27:38 +01:00
-- 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)
Prevent dangling-pointer access when update trigger returns old tuple. A before-update row trigger may choose to return the "new" or "old" tuple unmodified. ExecBRUpdateTriggers failed to consider the second possibility, and would proceed to free the "old" tuple even if it was the one returned, leading to subsequent access to already-deallocated memory. In debug builds this reliably leads to an "invalid memory alloc request size" failure; in production builds it might accidentally work, but data corruption is also possible. This is a very old bug. There are probably a couple of reasons it hasn't been noticed up to now. It would be more usual to return NULL if one wanted to suppress the update action; returning "old" is significantly less efficient since the update will occur anyway. Also, none of the standard PLs would ever cause this because they all returned freshly-manufactured tuples even if they were just copying "old". But commit 4b93f5799 changed that for plpgsql, making it possible to see the bug with a plpgsql trigger. Still, this is certainly legal behavior for a trigger function, so it's ExecBRUpdateTriggers's fault not plpgsql's. It seems worth creating a test case that exercises returning "old" directly with a C-language trigger; testing this through plpgsql seems unreliable because its behavior might change again. Report and fix by Rushabh Lathia; regression test case by me. Back-patch to all supported branches. Discussion: https://postgr.es/m/CAGPqQf1P4pjiNPrMof=P_16E-DFjt457j+nH2ex3=nBTew7tXw@mail.gmail.com
2018-02-27 19:27:38 +01:00
drop table trigtest;
2000-01-05 18:31:08 +01:00
create sequence ttdummy_seq increment 10 start 0 minvalue 0;
create table tttest (
price_id int4,
price_val int4,
price_on int4,
1997-09-24 10:36:47 +02:00
price_off int4 default 999999
);
create trigger ttdummy
1997-09-24 10:36:47 +02:00
before delete or update on tttest
for each row
execute procedure
1997-09-24 10:36:47 +02:00
ttdummy (price_on, price_off);
create trigger ttserial
before insert or update on tttest
for each row
execute procedure
autoinc (price_on, ttdummy_seq);
2000-01-05 18:31:08 +01:00
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
1997-09-24 10:36:47 +02:00
(3 rows)
2000-01-05 18:31:08 +01:00
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
1997-09-24 10:36:47 +02:00
(3 rows)
2000-01-05 18:31:08 +01:00
-- 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
1997-09-24 10:36:47 +02:00
(2 rows)
2000-01-05 18:31:08 +01:00
-- 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
1997-09-24 10:36:47 +02:00
(4 rows)
2000-01-05 18:31:08 +01:00
-- 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
1997-09-24 10:36:47 +02:00
(5 rows)
2000-01-05 18:31:08 +01:00
-- restore data as before last update:
select set_ttdummy(0);
set_ttdummy
-------------
1
1997-09-24 10:36:47 +02:00
(1 row)
2000-01-05 18:31:08 +01:00
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
1997-09-24 10:36:47 +02:00
(4 rows)
2000-01-05 18:31:08 +01:00
-- 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
1997-09-24 10:36:47 +02:00
(4 rows)
2000-01-05 18:31:08 +01:00
-- isn't it what we need ?
select set_ttdummy(1);
set_ttdummy
-------------
0
1997-09-24 10:36:47 +02:00
(1 row)
2000-01-05 18:31:08 +01:00
-- 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)
2000-01-05 18:31:08 +01:00
-- but this doesn't work
-- try in this way
select set_ttdummy(0);
set_ttdummy
-------------
1
1997-09-24 10:36:47 +02:00
(1 row)
2000-01-05 18:31:08 +01:00
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
1997-09-24 10:36:47 +02:00
(4 rows)
2000-01-05 18:31:08 +01:00
-- 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
1997-09-24 10:36:47 +02:00
(1 row)
2000-01-05 18:31:08 +01:00
drop table tttest;
drop sequence ttdummy_seq;
--
-- tests for per-statement triggers
--
CREATE TABLE log_table (tstamp timestamp default timeofday()::timestamp);
Add support for INSERT ... ON CONFLICT DO NOTHING/UPDATE. The newly added ON CONFLICT clause allows to specify an alternative to raising a unique or exclusion constraint violation error when inserting. ON CONFLICT refers to constraints that can either be specified using a inference clause (by specifying the columns of a unique constraint) or by naming a unique or exclusion constraint. DO NOTHING avoids the constraint violation, without touching the pre-existing row. DO UPDATE SET ... [WHERE ...] updates the pre-existing tuple, and has access to both the tuple proposed for insertion and the existing tuple; the optional WHERE clause can be used to prevent an update from being executed. The UPDATE SET and WHERE clauses have access to the tuple proposed for insertion using the "magic" EXCLUDED alias, and to the pre-existing tuple using the table name or its alias. This feature is often referred to as upsert. This is implemented using a new infrastructure called "speculative insertion". It is an optimistic variant of regular insertion that first does a pre-check for existing tuples and then attempts an insert. If a violating tuple was inserted concurrently, the speculatively inserted tuple is deleted and a new attempt is made. If the pre-check finds a matching tuple the alternative DO NOTHING or DO UPDATE action is taken. If the insertion succeeds without detecting a conflict, the tuple is deemed inserted. To handle the possible ambiguity between the excluded alias and a table named excluded, and for convenience with long relation names, INSERT INTO now can alias its target table. Bumps catversion as stored rules change. Author: Peter Geoghegan, with significant contributions from Heikki Linnakangas and Andres Freund. Testing infrastructure by Jeff Janes. Reviewed-By: Heikki Linnakangas, Andres Freund, Robert Haas, Simon Riggs, Dean Rasheed, Stephen Frost and many others.
2015-05-08 05:31:36 +02:00
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');
Add support for INSERT ... ON CONFLICT DO NOTHING/UPDATE. The newly added ON CONFLICT clause allows to specify an alternative to raising a unique or exclusion constraint violation error when inserting. ON CONFLICT refers to constraints that can either be specified using a inference clause (by specifying the columns of a unique constraint) or by naming a unique or exclusion constraint. DO NOTHING avoids the constraint violation, without touching the pre-existing row. DO UPDATE SET ... [WHERE ...] updates the pre-existing tuple, and has access to both the tuple proposed for insertion and the existing tuple; the optional WHERE clause can be used to prevent an update from being executed. The UPDATE SET and WHERE clauses have access to the tuple proposed for insertion using the "magic" EXCLUDED alias, and to the pre-existing tuple using the table name or its alias. This feature is often referred to as upsert. This is implemented using a new infrastructure called "speculative insertion". It is an optimistic variant of regular insertion that first does a pre-check for existing tuples and then attempts an insert. If a violating tuple was inserted concurrently, the speculatively inserted tuple is deleted and a new attempt is made. If the pre-check finds a matching tuple the alternative DO NOTHING or DO UPDATE action is taken. If the insertion succeeds without detecting a conflict, the tuple is deemed inserted. To handle the possible ambiguity between the excluded alias and a table named excluded, and for convenience with long relation names, INSERT INTO now can alias its target table. Bumps catversion as stored rules change. Author: Peter Geoghegan, with significant contributions from Heikki Linnakangas and Andres Freund. Testing infrastructure by Jeff Janes. Reviewed-By: Heikki Linnakangas, Andres Freund, Robert Haas, Simon Riggs, Dean Rasheed, Stephen Frost and many others.
2015-05-08 05:31:36 +02:00
-- 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
Add support for INSERT ... ON CONFLICT DO NOTHING/UPDATE. The newly added ON CONFLICT clause allows to specify an alternative to raising a unique or exclusion constraint violation error when inserting. ON CONFLICT refers to constraints that can either be specified using a inference clause (by specifying the columns of a unique constraint) or by naming a unique or exclusion constraint. DO NOTHING avoids the constraint violation, without touching the pre-existing row. DO UPDATE SET ... [WHERE ...] updates the pre-existing tuple, and has access to both the tuple proposed for insertion and the existing tuple; the optional WHERE clause can be used to prevent an update from being executed. The UPDATE SET and WHERE clauses have access to the tuple proposed for insertion using the "magic" EXCLUDED alias, and to the pre-existing tuple using the table name or its alias. This feature is often referred to as upsert. This is implemented using a new infrastructure called "speculative insertion". It is an optimistic variant of regular insertion that first does a pre-check for existing tuples and then attempts an insert. If a violating tuple was inserted concurrently, the speculatively inserted tuple is deleted and a new attempt is made. If the pre-check finds a matching tuple the alternative DO NOTHING or DO UPDATE action is taken. If the insertion succeeds without detecting a conflict, the tuple is deemed inserted. To handle the possible ambiguity between the excluded alias and a table named excluded, and for convenience with long relation names, INSERT INTO now can alias its target table. Bumps catversion as stored rules change. Author: Peter Geoghegan, with significant contributions from Heikki Linnakangas and Andres Freund. Testing infrastructure by Jeff Janes. Reviewed-By: Heikki Linnakangas, Andres Freund, Robert Haas, Simon Riggs, Dean Rasheed, Stephen Frost and many others.
2015-05-08 05:31:36 +02:00
-- 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.
Remove WITH OIDS support, change oid catalog column visibility. Previously tables declared WITH OIDS, including a significant fraction of the catalog tables, stored the oid column not as a normal column, but as part of the tuple header. This special column was not shown by default, which was somewhat odd, as it's often (consider e.g. pg_class.oid) one of the more important parts of a row. Neither pg_dump nor COPY included the contents of the oid column by default. The fact that the oid column was not an ordinary column necessitated a significant amount of special case code to support oid columns. That already was painful for the existing, but upcoming work aiming to make table storage pluggable, would have required expanding and duplicating that "specialness" significantly. WITH OIDS has been deprecated since 2005 (commit ff02d0a05280e0). Remove it. Removing includes: - CREATE TABLE and ALTER TABLE syntax for declaring the table to be WITH OIDS has been removed (WITH (oids[ = true]) will error out) - pg_dump does not support dumping tables declared WITH OIDS and will issue a warning when dumping one (and ignore the oid column). - restoring an pg_dump archive with pg_restore will warn when restoring a table with oid contents (and ignore the oid column) - COPY will refuse to load binary dump that includes oids. - pg_upgrade will error out when encountering tables declared WITH OIDS, they have to be altered to remove the oid column first. - Functionality to access the oid of the last inserted row (like plpgsql's RESULT_OID, spi's SPI_lastoid, ...) has been removed. The syntax for declaring a table WITHOUT OIDS (or WITH (oids = false) for CREATE TABLE) is still supported. While that requires a bit of support code, it seems unnecessary to break applications / dumps that do not use oids, and are explicit about not using them. The biggest user of WITH OID columns was postgres' catalog. This commit changes all 'magic' oid columns to be columns that are normally declared and stored. To reduce unnecessary query breakage all the newly added columns are still named 'oid', even if a table's column naming scheme would indicate 'reloid' or such. This obviously requires adapting a lot code, mostly replacing oid access via HeapTupleGetOid() with access to the underlying Form_pg_*->oid column. The bootstrap process now assigns oids for all oid columns in genbki.pl that do not have an explicit value (starting at the largest oid previously used), only oids assigned later by oids will be above FirstBootstrapObjectId. As the oid column now is a normal column the special bootstrap syntax for oids has been removed. Oids are not automatically assigned during insertion anymore, all backend code explicitly assigns oids with GetNewOidWithIndex(). For the rare case that insertions into the catalog via SQL are called for the new pg_nextoid() function can be used (which only works on catalog tables). The fact that oid columns on system tables are now normal columns means that they will be included in the set of columns expanded by * (i.e. SELECT * FROM pg_class will now include the table's oid, previously it did not). It'd not technically be hard to hide oid column by default, but that'd mean confusing behavior would either have to be carried forward forever, or it'd cause breakage down the line. While it's not unlikely that further adjustments are needed, the scope/invasiveness of the patch makes it worthwhile to get merge this now. It's painful to maintain externally, too complicated to commit after the code code freeze, and a dependency of a number of other patches. Catversion bump, for obvious reasons. Author: Andres Freund, with contributions by John Naylor Discussion: https://postgr.es/m/20180930034810.ywp2c7awz7opzcfr@alap3.anarazel.de
2018-11-21 00:36:57 +01:00
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
Remove WITH OIDS support, change oid catalog column visibility. Previously tables declared WITH OIDS, including a significant fraction of the catalog tables, stored the oid column not as a normal column, but as part of the tuple header. This special column was not shown by default, which was somewhat odd, as it's often (consider e.g. pg_class.oid) one of the more important parts of a row. Neither pg_dump nor COPY included the contents of the oid column by default. The fact that the oid column was not an ordinary column necessitated a significant amount of special case code to support oid columns. That already was painful for the existing, but upcoming work aiming to make table storage pluggable, would have required expanding and duplicating that "specialness" significantly. WITH OIDS has been deprecated since 2005 (commit ff02d0a05280e0). Remove it. Removing includes: - CREATE TABLE and ALTER TABLE syntax for declaring the table to be WITH OIDS has been removed (WITH (oids[ = true]) will error out) - pg_dump does not support dumping tables declared WITH OIDS and will issue a warning when dumping one (and ignore the oid column). - restoring an pg_dump archive with pg_restore will warn when restoring a table with oid contents (and ignore the oid column) - COPY will refuse to load binary dump that includes oids. - pg_upgrade will error out when encountering tables declared WITH OIDS, they have to be altered to remove the oid column first. - Functionality to access the oid of the last inserted row (like plpgsql's RESULT_OID, spi's SPI_lastoid, ...) has been removed. The syntax for declaring a table WITHOUT OIDS (or WITH (oids = false) for CREATE TABLE) is still supported. While that requires a bit of support code, it seems unnecessary to break applications / dumps that do not use oids, and are explicit about not using them. The biggest user of WITH OID columns was postgres' catalog. This commit changes all 'magic' oid columns to be columns that are normally declared and stored. To reduce unnecessary query breakage all the newly added columns are still named 'oid', even if a table's column naming scheme would indicate 'reloid' or such. This obviously requires adapting a lot code, mostly replacing oid access via HeapTupleGetOid() with access to the underlying Form_pg_*->oid column. The bootstrap process now assigns oids for all oid columns in genbki.pl that do not have an explicit value (starting at the largest oid previously used), only oids assigned later by oids will be above FirstBootstrapObjectId. As the oid column now is a normal column the special bootstrap syntax for oids has been removed. Oids are not automatically assigned during insertion anymore, all backend code explicitly assigns oids with GetNewOidWithIndex(). For the rare case that insertions into the catalog via SQL are called for the new pg_nextoid() function can be used (which only works on catalog tables). The fact that oid columns on system tables are now normal columns means that they will be included in the set of columns expanded by * (i.e. SELECT * FROM pg_class will now include the table's oid, previously it did not). It'd not technically be hard to hide oid column by default, but that'd mean confusing behavior would either have to be carried forward forever, or it'd cause breakage down the line. While it's not unlikely that further adjustments are needed, the scope/invasiveness of the patch makes it worthwhile to get merge this now. It's painful to maintain externally, too complicated to commit after the code code freeze, and a dependency of a number of other patches. Catversion bump, for obvious reasons. Author: Andres Freund, with contributions by John Naylor Discussion: https://postgr.es/m/20180930034810.ywp2c7awz7opzcfr@alap3.anarazel.de
2018-11-21 00:36:57 +01:00
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;
2006-05-28 05:12:00 +02:00
-- 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;
2006-05-28 05:12:00 +02:00
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:
Get rid of the "new" and "old" entries in a view's rangetable. The rule system needs "old" and/or "new" pseudo-RTEs in rule actions that are ON INSERT/UPDATE/DELETE. Historically it's put such entries into the ON SELECT rules of views as well, but those are really quite vestigial. The only thing we've used them for is to carry the view's relid forward to AcquireExecutorLocks (so that we can re-lock the view to verify it hasn't changed before re-using a plan) and to carry its relid and permissions data forward to execution-time permissions checks. What we can do instead of that is to retain these fields of the RTE_RELATION RTE for the view even after we convert it to an RTE_SUBQUERY RTE. This requires a tiny amount of extra complication in the planner and AcquireExecutorLocks, but on the other hand we can get rid of the logic that moves that data from one place to another. The principal immediate benefit of doing this, aside from a small saving in the pg_rewrite data for views, is that these pseudo-RTEs no longer trigger ruleutils.c's heuristic about qualifying variable names when the rangetable's length is more than 1. That results in quite a number of small simplifications in regression test outputs, which are all to the good IMO. Bump catversion because we need to dump a few more fields of RTE_SUBQUERY RTEs. While those will always be zeroes anyway in stored rules (because we'd never populate them until query rewrite) they are useful for debugging, and it seems like we'd better make sure to transmit such RTEs accurately in plans sent to parallel workers. I don't think the executor actually examines these fields after startup, but someday it might. This is a second attempt at committing 1b4d280ea. The difference from the first time is that now we can add some filtering rules to AdjustUpgrade.pm to allow cross-version upgrade testing to pass despite all the cosmetic changes in CREATE VIEW outputs. Amit Langote (filtering rules by me) Discussion: https://postgr.es/m/CA+HiwqEf7gPN4Hn+LoZ4tP2q_Qt7n3vw7-6fJKOf92tSEnX6Gg@mail.gmail.com Discussion: https://postgr.es/m/891521.1673657296@sss.pgh.pa.us
2023-01-18 19:23:57 +01:00
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
Fix a number of issues around modifying a previously updated row. This commit fixes three, unfortunately related, issues: 1) Since 5db6df0c01, the introduction of DML via tableam, it was possible to trigger "ERROR: unexpected table_lock_tuple status: 1" when updating a row that was previously updated in the same transaction - but only when the previously updated row was before updated in a concurrent transaction (and READ COMMITTED was used). The reason for that was that that case simply wasn't expected. Fixing that lead to: 2) Even before the above commit, there were error checks (introduced in 6868ed7491b7) preventing a row being updated by different commands within the same statement (say in a function called by an UPDATE) - but that check wasn't performed when the row was first updated in a concurrent transaction - instead the second update was silently skipped in that case. After this change we throw the same error as we'd without the concurrent transaction. 3) The error messages (introduced in 6868ed7491b7) preventing such updates emitted the same error message for both DELETE and UPDATE ("tuple to be updated was already modified by an operation triggered by the current command"). While that could be changed separately, it made it hard to write tests that verify the correct correct behavior of the code. This commit changes heap's implementation of table_lock_tuple() to return TM_SelfModified instead of TM_Invisible (previously loosely modeled after EvalPlanQualFetch), and teaches nodeModifyTable.c to handle that in response to table_lock_tuple() and not just in response to table_(delete|update). Additionally it fixes the wrong error message (see 3 above). The comment for table_lock_tuple() is also adjusted to state that TM_Deleted won't return information in TM_FailureData - it'll not always be available. This also adds tests to ensure that DELETE/UPDATE correctly error out when affecting a row that concurrently was modified by another transaction. Author: Andres Freund Reported-By: Tom Lane, when investigating a bug bug fix to another bug by Amit Langote Discussion: https://postgr.es/m/19321.1554567786@sss.pgh.pa.us
2019-04-08 07:14:47 +02:00
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();
Add support for INSERT ... ON CONFLICT DO NOTHING/UPDATE. The newly added ON CONFLICT clause allows to specify an alternative to raising a unique or exclusion constraint violation error when inserting. ON CONFLICT refers to constraints that can either be specified using a inference clause (by specifying the columns of a unique constraint) or by naming a unique or exclusion constraint. DO NOTHING avoids the constraint violation, without touching the pre-existing row. DO UPDATE SET ... [WHERE ...] updates the pre-existing tuple, and has access to both the tuple proposed for insertion and the existing tuple; the optional WHERE clause can be used to prevent an update from being executed. The UPDATE SET and WHERE clauses have access to the tuple proposed for insertion using the "magic" EXCLUDED alias, and to the pre-existing tuple using the table name or its alias. This feature is often referred to as upsert. This is implemented using a new infrastructure called "speculative insertion". It is an optimistic variant of regular insertion that first does a pre-check for existing tuples and then attempts an insert. If a violating tuple was inserted concurrently, the speculatively inserted tuple is deleted and a new attempt is made. If the pre-check finds a matching tuple the alternative DO NOTHING or DO UPDATE action is taken. If the insertion succeeds without detecting a conflict, the tuple is deemed inserted. To handle the possible ambiguity between the excluded alias and a table named excluded, and for convenience with long relation names, INSERT INTO now can alias its target table. Bumps catversion as stored rules change. Author: Peter Geoghegan, with significant contributions from Heikki Linnakangas and Andres Freund. Testing infrastructure by Jeff Janes. Reviewed-By: Heikki Linnakangas, Andres Freund, Robert Haas, Simon Riggs, Dean Rasheed, Stephen Frost and many others.
2015-05-08 05:31:36 +02:00
--
-- 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();
--
Add support for INSERT ... ON CONFLICT DO NOTHING/UPDATE. The newly added ON CONFLICT clause allows to specify an alternative to raising a unique or exclusion constraint violation error when inserting. ON CONFLICT refers to constraints that can either be specified using a inference clause (by specifying the columns of a unique constraint) or by naming a unique or exclusion constraint. DO NOTHING avoids the constraint violation, without touching the pre-existing row. DO UPDATE SET ... [WHERE ...] updates the pre-existing tuple, and has access to both the tuple proposed for insertion and the existing tuple; the optional WHERE clause can be used to prevent an update from being executed. The UPDATE SET and WHERE clauses have access to the tuple proposed for insertion using the "magic" EXCLUDED alias, and to the pre-existing tuple using the table name or its alias. This feature is often referred to as upsert. This is implemented using a new infrastructure called "speculative insertion". It is an optimistic variant of regular insertion that first does a pre-check for existing tuples and then attempts an insert. If a violating tuple was inserted concurrently, the speculatively inserted tuple is deleted and a new attempt is made. If the pre-check finds a matching tuple the alternative DO NOTHING or DO UPDATE action is taken. If the insertion succeeds without detecting a conflict, the tuple is deemed inserted. To handle the possible ambiguity between the excluded alias and a table named excluded, and for convenience with long relation names, INSERT INTO now can alias its target table. Bumps catversion as stored rules change. Author: Peter Geoghegan, with significant contributions from Heikki Linnakangas and Andres Freund. Testing infrastructure by Jeff Janes. Reviewed-By: Heikki Linnakangas, Andres Freund, Robert Haas, Simon Riggs, Dean Rasheed, Stephen Frost and many others.
2015-05-08 05:31:36 +02:00
-- 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;
Add support for INSERT ... ON CONFLICT DO NOTHING/UPDATE. The newly added ON CONFLICT clause allows to specify an alternative to raising a unique or exclusion constraint violation error when inserting. ON CONFLICT refers to constraints that can either be specified using a inference clause (by specifying the columns of a unique constraint) or by naming a unique or exclusion constraint. DO NOTHING avoids the constraint violation, without touching the pre-existing row. DO UPDATE SET ... [WHERE ...] updates the pre-existing tuple, and has access to both the tuple proposed for insertion and the existing tuple; the optional WHERE clause can be used to prevent an update from being executed. The UPDATE SET and WHERE clauses have access to the tuple proposed for insertion using the "magic" EXCLUDED alias, and to the pre-existing tuple using the table name or its alias. This feature is often referred to as upsert. This is implemented using a new infrastructure called "speculative insertion". It is an optimistic variant of regular insertion that first does a pre-check for existing tuples and then attempts an insert. If a violating tuple was inserted concurrently, the speculatively inserted tuple is deleted and a new attempt is made. If the pre-check finds a matching tuple the alternative DO NOTHING or DO UPDATE action is taken. If the insertion succeeds without detecting a conflict, the tuple is deemed inserted. To handle the possible ambiguity between the excluded alias and a table named excluded, and for convenience with long relation names, INSERT INTO now can alias its target table. Bumps catversion as stored rules change. Author: Peter Geoghegan, with significant contributions from Heikki Linnakangas and Andres Freund. Testing infrastructure by Jeff Janes. Reviewed-By: Heikki Linnakangas, Andres Freund, Robert Haas, Simon Riggs, Dean Rasheed, Stephen Frost and many others.
2015-05-08 05:31:36 +02:00
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")
Add support for INSERT ... ON CONFLICT DO NOTHING/UPDATE. The newly added ON CONFLICT clause allows to specify an alternative to raising a unique or exclusion constraint violation error when inserting. ON CONFLICT refers to constraints that can either be specified using a inference clause (by specifying the columns of a unique constraint) or by naming a unique or exclusion constraint. DO NOTHING avoids the constraint violation, without touching the pre-existing row. DO UPDATE SET ... [WHERE ...] updates the pre-existing tuple, and has access to both the tuple proposed for insertion and the existing tuple; the optional WHERE clause can be used to prevent an update from being executed. The UPDATE SET and WHERE clauses have access to the tuple proposed for insertion using the "magic" EXCLUDED alias, and to the pre-existing tuple using the table name or its alias. This feature is often referred to as upsert. This is implemented using a new infrastructure called "speculative insertion". It is an optimistic variant of regular insertion that first does a pre-check for existing tuples and then attempts an insert. If a violating tuple was inserted concurrently, the speculatively inserted tuple is deleted and a new attempt is made. If the pre-check finds a matching tuple the alternative DO NOTHING or DO UPDATE action is taken. If the insertion succeeds without detecting a conflict, the tuple is deemed inserted. To handle the possible ambiguity between the excluded alias and a table named excluded, and for convenience with long relation names, INSERT INTO now can alias its target table. Bumps catversion as stored rules change. Author: Peter Geoghegan, with significant contributions from Heikki Linnakangas and Andres Freund. Testing infrastructure by Jeff Janes. Reviewed-By: Heikki Linnakangas, Andres Freund, Robert Haas, Simon Riggs, Dean Rasheed, Stephen Frost and many others.
2015-05-08 05:31:36 +02:00
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")
Add support for INSERT ... ON CONFLICT DO NOTHING/UPDATE. The newly added ON CONFLICT clause allows to specify an alternative to raising a unique or exclusion constraint violation error when inserting. ON CONFLICT refers to constraints that can either be specified using a inference clause (by specifying the columns of a unique constraint) or by naming a unique or exclusion constraint. DO NOTHING avoids the constraint violation, without touching the pre-existing row. DO UPDATE SET ... [WHERE ...] updates the pre-existing tuple, and has access to both the tuple proposed for insertion and the existing tuple; the optional WHERE clause can be used to prevent an update from being executed. The UPDATE SET and WHERE clauses have access to the tuple proposed for insertion using the "magic" EXCLUDED alias, and to the pre-existing tuple using the table name or its alias. This feature is often referred to as upsert. This is implemented using a new infrastructure called "speculative insertion". It is an optimistic variant of regular insertion that first does a pre-check for existing tuples and then attempts an insert. If a violating tuple was inserted concurrently, the speculatively inserted tuple is deleted and a new attempt is made. If the pre-check finds a matching tuple the alternative DO NOTHING or DO UPDATE action is taken. If the insertion succeeds without detecting a conflict, the tuple is deemed inserted. To handle the possible ambiguity between the excluded alias and a table named excluded, and for convenience with long relation names, INSERT INTO now can alias its target table. Bumps catversion as stored rules change. Author: Peter Geoghegan, with significant contributions from Heikki Linnakangas and Andres Freund. Testing infrastructure by Jeff Janes. Reviewed-By: Heikki Linnakangas, Andres Freund, Robert Haas, Simon Riggs, Dean Rasheed, Stephen Frost and many others.
2015-05-08 05:31:36 +02:00
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")
Add support for INSERT ... ON CONFLICT DO NOTHING/UPDATE. The newly added ON CONFLICT clause allows to specify an alternative to raising a unique or exclusion constraint violation error when inserting. ON CONFLICT refers to constraints that can either be specified using a inference clause (by specifying the columns of a unique constraint) or by naming a unique or exclusion constraint. DO NOTHING avoids the constraint violation, without touching the pre-existing row. DO UPDATE SET ... [WHERE ...] updates the pre-existing tuple, and has access to both the tuple proposed for insertion and the existing tuple; the optional WHERE clause can be used to prevent an update from being executed. The UPDATE SET and WHERE clauses have access to the tuple proposed for insertion using the "magic" EXCLUDED alias, and to the pre-existing tuple using the table name or its alias. This feature is often referred to as upsert. This is implemented using a new infrastructure called "speculative insertion". It is an optimistic variant of regular insertion that first does a pre-check for existing tuples and then attempts an insert. If a violating tuple was inserted concurrently, the speculatively inserted tuple is deleted and a new attempt is made. If the pre-check finds a matching tuple the alternative DO NOTHING or DO UPDATE action is taken. If the insertion succeeds without detecting a conflict, the tuple is deemed inserted. To handle the possible ambiguity between the excluded alias and a table named excluded, and for convenience with long relation names, INSERT INTO now can alias its target table. Bumps catversion as stored rules change. Author: Peter Geoghegan, with significant contributions from Heikki Linnakangas and Andres Freund. Testing infrastructure by Jeff Janes. Reviewed-By: Heikki Linnakangas, Andres Freund, Robert Haas, Simon Riggs, Dean Rasheed, Stephen Frost and many others.
2015-05-08 05:31:36 +02:00
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 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)
Fix SQL-spec incompatibilities in new transition table feature. The standard says that all changes of the same kind (insert, update, or delete) caused in one table by a single SQL statement should be reported in a single transition table; and by that, they mean to include foreign key enforcement actions cascading from the statement's direct effects. It's also reasonable to conclude that if the standard had wCTEs, they would say that effects of wCTEs applying to the same table as each other or the outer statement should be merged into one transition table. We weren't doing it like that. Hence, arrange to merge tuples from multiple update actions into a single transition table as much as we can. There is a problem, which is that if the firing of FK enforcement triggers and after-row triggers with transition tables is interspersed, we might need to report more tuples after some triggers have already seen the transition table. It seems like a bad idea for the transition table to be mutable between trigger calls. There's no good way around this without a major redesign of the FK logic, so for now, resolve it by opening a new transition table each time this happens. Also, ensure that AFTER STATEMENT triggers fire just once per statement, or once per transition table when we're forced to make more than one. Previous versions of Postgres have allowed each FK enforcement query to cause an additional firing of the AFTER STATEMENT triggers for the referencing table, but that's certainly not per spec. (We're still doing multiple firings of BEFORE STATEMENT triggers, though; is that something worth changing?) Also, forbid using transition tables with column-specific UPDATE triggers. The spec requires such transition tables to show only the tuples for which the UPDATE trigger would have fired, which means maintaining multiple transition tables or else somehow filtering the contents at readout. Maybe someday we'll bother to support that option, but it looks like a lot of trouble for a marginal feature. The transition tables are now managed by the AfterTriggers data structures, rather than being directly the responsibility of ModifyTable nodes. This removes a subtransaction-lifespan memory leak introduced by my previous band-aid patch 3c4359521. In passing, refactor the AfterTriggers data structures to reduce the management overhead for them, by using arrays of structs rather than several parallel arrays for per-query-level and per-subtransaction state. I failed to resist the temptation to do some copy-editing on the SGML docs about triggers, above and beyond merely documenting the effects of this patch. Back-patch to v10, because we don't want the semantics of transition tables to change post-release. Patch by me, with help and review from Thomas Munro. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-16 19:20:32 +02:00
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();
2017-09-11 17:20:47 +02:00
ERROR: transition tables cannot be specified for triggers with more than one event
Fix SQL-spec incompatibilities in new transition table feature. The standard says that all changes of the same kind (insert, update, or delete) caused in one table by a single SQL statement should be reported in a single transition table; and by that, they mean to include foreign key enforcement actions cascading from the statement's direct effects. It's also reasonable to conclude that if the standard had wCTEs, they would say that effects of wCTEs applying to the same table as each other or the outer statement should be merged into one transition table. We weren't doing it like that. Hence, arrange to merge tuples from multiple update actions into a single transition table as much as we can. There is a problem, which is that if the firing of FK enforcement triggers and after-row triggers with transition tables is interspersed, we might need to report more tuples after some triggers have already seen the transition table. It seems like a bad idea for the transition table to be mutable between trigger calls. There's no good way around this without a major redesign of the FK logic, so for now, resolve it by opening a new transition table each time this happens. Also, ensure that AFTER STATEMENT triggers fire just once per statement, or once per transition table when we're forced to make more than one. Previous versions of Postgres have allowed each FK enforcement query to cause an additional firing of the AFTER STATEMENT triggers for the referencing table, but that's certainly not per spec. (We're still doing multiple firings of BEFORE STATEMENT triggers, though; is that something worth changing?) Also, forbid using transition tables with column-specific UPDATE triggers. The spec requires such transition tables to show only the tuples for which the UPDATE trigger would have fired, which means maintaining multiple transition tables or else somehow filtering the contents at readout. Maybe someday we'll bother to support that option, but it looks like a lot of trouble for a marginal feature. The transition tables are now managed by the AfterTriggers data structures, rather than being directly the responsibility of ModifyTable nodes. This removes a subtransaction-lifespan memory leak introduced by my previous band-aid patch 3c4359521. In passing, refactor the AfterTriggers data structures to reduce the management overhead for them, by using arrays of structs rather than several parallel arrays for per-query-level and per-subtransaction state. I failed to resist the temptation to do some copy-editing on the SGML docs about triggers, above and beyond merely documenting the effects of this patch. Back-patch to v10, because we don't want the semantics of transition tables to change post-release. Patch by me, with help and review from Thomas Munro. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-16 19:20:32 +02:00
--
-- 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;
Quick-hack fix for foreign key cascade vs triggers with transition tables. AFTER triggers using transition tables crashed if they were fired due to a foreign key ON CASCADE update. This is because ExecEndModifyTable flushes the transition tables, on the assumption that any trigger that could need them was already fired during ExecutorFinish. Normally that's true, because we don't allow transition-table-using triggers to be deferred. However, foreign key CASCADE updates force any triggers on the referencing table to be deferred to the outer query level, by means of the EXEC_FLAG_SKIP_TRIGGERS flag. I don't recall all the details of why it's like that and am pretty loath to redesign it right now. Instead, just teach ExecEndModifyTable to skip destroying the TransitionCaptureState when that flag is set. This will allow the transition table data to survive until end of the current subtransaction. This isn't a terribly satisfactory solution, because (1) we might be leaking the transition tables for much longer than really necessary, and (2) as things stand, an AFTER STATEMENT trigger will fire once per RI updating query, ie once per row updated or deleted in the referenced table. I suspect that is not per SQL spec. But redesigning this is a research project that we're certainly not going to get done for v10. So let's go with this hackish answer for now. In passing, tweak AfterTriggerSaveEvent to not save the transition_capture pointer into the event record for a deferrable trigger. This is not necessary to fix the current bug, but it avoids letting dangling pointers to long-gone transition tables persist in the trigger event queue. That's at least a safety feature. It might also allow merging shared trigger states in more cases than before. I added a regression test that demonstrates the crash on unpatched code, and also exposes the behavior of firing the AFTER STATEMENT triggers once per row update. Per bug #14808 from Philippe Beaudoin. Back-patch to v10. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-10 20:59:56 +02:00
--
-- 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');
Quick-hack fix for foreign key cascade vs triggers with transition tables. AFTER triggers using transition tables crashed if they were fired due to a foreign key ON CASCADE update. This is because ExecEndModifyTable flushes the transition tables, on the assumption that any trigger that could need them was already fired during ExecutorFinish. Normally that's true, because we don't allow transition-table-using triggers to be deferred. However, foreign key CASCADE updates force any triggers on the referencing table to be deferred to the outer query level, by means of the EXEC_FLAG_SKIP_TRIGGERS flag. I don't recall all the details of why it's like that and am pretty loath to redesign it right now. Instead, just teach ExecEndModifyTable to skip destroying the TransitionCaptureState when that flag is set. This will allow the transition table data to survive until end of the current subtransaction. This isn't a terribly satisfactory solution, because (1) we might be leaking the transition tables for much longer than really necessary, and (2) as things stand, an AFTER STATEMENT trigger will fire once per RI updating query, ie once per row updated or deleted in the referenced table. I suspect that is not per SQL spec. But redesigning this is a research project that we're certainly not going to get done for v10. So let's go with this hackish answer for now. In passing, tweak AfterTriggerSaveEvent to not save the transition_capture pointer into the event record for a deferrable trigger. This is not necessary to fix the current bug, but it avoids letting dangling pointers to long-gone transition tables persist in the trigger event queue. That's at least a safety feature. It might also allow merging shared trigger states in more cases than before. I added a regression test that demonstrates the crash on unpatched code, and also exposes the behavior of firing the AFTER STATEMENT triggers once per row update. Per bug #14808 from Philippe Beaudoin. Back-patch to v10. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-10 20:59:56 +02:00
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
Quick-hack fix for foreign key cascade vs triggers with transition tables. AFTER triggers using transition tables crashed if they were fired due to a foreign key ON CASCADE update. This is because ExecEndModifyTable flushes the transition tables, on the assumption that any trigger that could need them was already fired during ExecutorFinish. Normally that's true, because we don't allow transition-table-using triggers to be deferred. However, foreign key CASCADE updates force any triggers on the referencing table to be deferred to the outer query level, by means of the EXEC_FLAG_SKIP_TRIGGERS flag. I don't recall all the details of why it's like that and am pretty loath to redesign it right now. Instead, just teach ExecEndModifyTable to skip destroying the TransitionCaptureState when that flag is set. This will allow the transition table data to survive until end of the current subtransaction. This isn't a terribly satisfactory solution, because (1) we might be leaking the transition tables for much longer than really necessary, and (2) as things stand, an AFTER STATEMENT trigger will fire once per RI updating query, ie once per row updated or deleted in the referenced table. I suspect that is not per SQL spec. But redesigning this is a research project that we're certainly not going to get done for v10. So let's go with this hackish answer for now. In passing, tweak AfterTriggerSaveEvent to not save the transition_capture pointer into the event record for a deferrable trigger. This is not necessary to fix the current bug, but it avoids letting dangling pointers to long-gone transition tables persist in the trigger event queue. That's at least a safety feature. It might also allow merging shared trigger states in more cases than before. I added a regression test that demonstrates the crash on unpatched code, and also exposes the behavior of firing the AFTER STATEMENT triggers once per row update. Per bug #14808 from Philippe Beaudoin. Back-patch to v10. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-10 20:59:56 +02:00
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
Quick-hack fix for foreign key cascade vs triggers with transition tables. AFTER triggers using transition tables crashed if they were fired due to a foreign key ON CASCADE update. This is because ExecEndModifyTable flushes the transition tables, on the assumption that any trigger that could need them was already fired during ExecutorFinish. Normally that's true, because we don't allow transition-table-using triggers to be deferred. However, foreign key CASCADE updates force any triggers on the referencing table to be deferred to the outer query level, by means of the EXEC_FLAG_SKIP_TRIGGERS flag. I don't recall all the details of why it's like that and am pretty loath to redesign it right now. Instead, just teach ExecEndModifyTable to skip destroying the TransitionCaptureState when that flag is set. This will allow the transition table data to survive until end of the current subtransaction. This isn't a terribly satisfactory solution, because (1) we might be leaking the transition tables for much longer than really necessary, and (2) as things stand, an AFTER STATEMENT trigger will fire once per RI updating query, ie once per row updated or deleted in the referenced table. I suspect that is not per SQL spec. But redesigning this is a research project that we're certainly not going to get done for v10. So let's go with this hackish answer for now. In passing, tweak AfterTriggerSaveEvent to not save the transition_capture pointer into the event record for a deferrable trigger. This is not necessary to fix the current bug, but it avoids letting dangling pointers to long-gone transition tables persist in the trigger event queue. That's at least a safety feature. It might also allow merging shared trigger states in more cases than before. I added a regression test that demonstrates the crash on unpatched code, and also exposes the behavior of firing the AFTER STATEMENT triggers once per row update. Per bug #14808 from Philippe Beaudoin. Back-patch to v10. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-10 20:59:56 +02:00
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
Fix SQL-spec incompatibilities in new transition table feature. The standard says that all changes of the same kind (insert, update, or delete) caused in one table by a single SQL statement should be reported in a single transition table; and by that, they mean to include foreign key enforcement actions cascading from the statement's direct effects. It's also reasonable to conclude that if the standard had wCTEs, they would say that effects of wCTEs applying to the same table as each other or the outer statement should be merged into one transition table. We weren't doing it like that. Hence, arrange to merge tuples from multiple update actions into a single transition table as much as we can. There is a problem, which is that if the firing of FK enforcement triggers and after-row triggers with transition tables is interspersed, we might need to report more tuples after some triggers have already seen the transition table. It seems like a bad idea for the transition table to be mutable between trigger calls. There's no good way around this without a major redesign of the FK logic, so for now, resolve it by opening a new transition table each time this happens. Also, ensure that AFTER STATEMENT triggers fire just once per statement, or once per transition table when we're forced to make more than one. Previous versions of Postgres have allowed each FK enforcement query to cause an additional firing of the AFTER STATEMENT triggers for the referencing table, but that's certainly not per spec. (We're still doing multiple firings of BEFORE STATEMENT triggers, though; is that something worth changing?) Also, forbid using transition tables with column-specific UPDATE triggers. The spec requires such transition tables to show only the tuples for which the UPDATE trigger would have fired, which means maintaining multiple transition tables or else somehow filtering the contents at readout. Maybe someday we'll bother to support that option, but it looks like a lot of trouble for a marginal feature. The transition tables are now managed by the AfterTriggers data structures, rather than being directly the responsibility of ModifyTable nodes. This removes a subtransaction-lifespan memory leak introduced by my previous band-aid patch 3c4359521. In passing, refactor the AfterTriggers data structures to reduce the management overhead for them, by using arrays of structs rather than several parallel arrays for per-query-level and per-subtransaction state. I failed to resist the temptation to do some copy-editing on the SGML docs about triggers, above and beyond merely documenting the effects of this patch. Back-patch to v10, because we don't want the semantics of transition tables to change post-release. Patch by me, with help and review from Thomas Munro. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-16 19:20:32 +02:00
NOTICE: trigger = trig_table_delete_trig, old table = (2,"two a"), (2,"two b"), (11,"one a"), (11,"one b")
Quick-hack fix for foreign key cascade vs triggers with transition tables. AFTER triggers using transition tables crashed if they were fired due to a foreign key ON CASCADE update. This is because ExecEndModifyTable flushes the transition tables, on the assumption that any trigger that could need them was already fired during ExecutorFinish. Normally that's true, because we don't allow transition-table-using triggers to be deferred. However, foreign key CASCADE updates force any triggers on the referencing table to be deferred to the outer query level, by means of the EXEC_FLAG_SKIP_TRIGGERS flag. I don't recall all the details of why it's like that and am pretty loath to redesign it right now. Instead, just teach ExecEndModifyTable to skip destroying the TransitionCaptureState when that flag is set. This will allow the transition table data to survive until end of the current subtransaction. This isn't a terribly satisfactory solution, because (1) we might be leaking the transition tables for much longer than really necessary, and (2) as things stand, an AFTER STATEMENT trigger will fire once per RI updating query, ie once per row updated or deleted in the referenced table. I suspect that is not per SQL spec. But redesigning this is a research project that we're certainly not going to get done for v10. So let's go with this hackish answer for now. In passing, tweak AfterTriggerSaveEvent to not save the transition_capture pointer into the event record for a deferrable trigger. This is not necessary to fix the current bug, but it avoids letting dangling pointers to long-gone transition tables persist in the trigger event queue. That's at least a safety feature. It might also allow merging shared trigger states in more cases than before. I added a regression test that demonstrates the crash on unpatched code, and also exposes the behavior of firing the AFTER STATEMENT triggers once per row update. Per bug #14808 from Philippe Beaudoin. Back-patch to v10. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-10 20:59:56 +02:00
select * from trig_table;
a | b
---+---------
3 | three a
3 | three b
(2 rows)
drop table refd_table, trig_table;
Fix SQL-spec incompatibilities in new transition table feature. The standard says that all changes of the same kind (insert, update, or delete) caused in one table by a single SQL statement should be reported in a single transition table; and by that, they mean to include foreign key enforcement actions cascading from the statement's direct effects. It's also reasonable to conclude that if the standard had wCTEs, they would say that effects of wCTEs applying to the same table as each other or the outer statement should be merged into one transition table. We weren't doing it like that. Hence, arrange to merge tuples from multiple update actions into a single transition table as much as we can. There is a problem, which is that if the firing of FK enforcement triggers and after-row triggers with transition tables is interspersed, we might need to report more tuples after some triggers have already seen the transition table. It seems like a bad idea for the transition table to be mutable between trigger calls. There's no good way around this without a major redesign of the FK logic, so for now, resolve it by opening a new transition table each time this happens. Also, ensure that AFTER STATEMENT triggers fire just once per statement, or once per transition table when we're forced to make more than one. Previous versions of Postgres have allowed each FK enforcement query to cause an additional firing of the AFTER STATEMENT triggers for the referencing table, but that's certainly not per spec. (We're still doing multiple firings of BEFORE STATEMENT triggers, though; is that something worth changing?) Also, forbid using transition tables with column-specific UPDATE triggers. The spec requires such transition tables to show only the tuples for which the UPDATE trigger would have fired, which means maintaining multiple transition tables or else somehow filtering the contents at readout. Maybe someday we'll bother to support that option, but it looks like a lot of trouble for a marginal feature. The transition tables are now managed by the AfterTriggers data structures, rather than being directly the responsibility of ModifyTable nodes. This removes a subtransaction-lifespan memory leak introduced by my previous band-aid patch 3c4359521. In passing, refactor the AfterTriggers data structures to reduce the management overhead for them, by using arrays of structs rather than several parallel arrays for per-query-level and per-subtransaction state. I failed to resist the temptation to do some copy-editing on the SGML docs about triggers, above and beyond merely documenting the effects of this patch. Back-patch to v10, because we don't want the semantics of transition tables to change post-release. Patch by me, with help and review from Thomas Munro. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-16 19:20:32 +02:00
--
-- 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');
Fix SQL-spec incompatibilities in new transition table feature. The standard says that all changes of the same kind (insert, update, or delete) caused in one table by a single SQL statement should be reported in a single transition table; and by that, they mean to include foreign key enforcement actions cascading from the statement's direct effects. It's also reasonable to conclude that if the standard had wCTEs, they would say that effects of wCTEs applying to the same table as each other or the outer statement should be merged into one transition table. We weren't doing it like that. Hence, arrange to merge tuples from multiple update actions into a single transition table as much as we can. There is a problem, which is that if the firing of FK enforcement triggers and after-row triggers with transition tables is interspersed, we might need to report more tuples after some triggers have already seen the transition table. It seems like a bad idea for the transition table to be mutable between trigger calls. There's no good way around this without a major redesign of the FK logic, so for now, resolve it by opening a new transition table each time this happens. Also, ensure that AFTER STATEMENT triggers fire just once per statement, or once per transition table when we're forced to make more than one. Previous versions of Postgres have allowed each FK enforcement query to cause an additional firing of the AFTER STATEMENT triggers for the referencing table, but that's certainly not per spec. (We're still doing multiple firings of BEFORE STATEMENT triggers, though; is that something worth changing?) Also, forbid using transition tables with column-specific UPDATE triggers. The spec requires such transition tables to show only the tuples for which the UPDATE trigger would have fired, which means maintaining multiple transition tables or else somehow filtering the contents at readout. Maybe someday we'll bother to support that option, but it looks like a lot of trouble for a marginal feature. The transition tables are now managed by the AfterTriggers data structures, rather than being directly the responsibility of ModifyTable nodes. This removes a subtransaction-lifespan memory leak introduced by my previous band-aid patch 3c4359521. In passing, refactor the AfterTriggers data structures to reduce the management overhead for them, by using arrays of structs rather than several parallel arrays for per-query-level and per-subtransaction state. I failed to resist the temptation to do some copy-editing on the SGML docs about triggers, above and beyond merely documenting the effects of this patch. Back-patch to v10, because we don't want the semantics of transition tables to change post-release. Patch by me, with help and review from Thomas Munro. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-16 19:20:32 +02:00
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
Fix SQL-spec incompatibilities in new transition table feature. The standard says that all changes of the same kind (insert, update, or delete) caused in one table by a single SQL statement should be reported in a single transition table; and by that, they mean to include foreign key enforcement actions cascading from the statement's direct effects. It's also reasonable to conclude that if the standard had wCTEs, they would say that effects of wCTEs applying to the same table as each other or the outer statement should be merged into one transition table. We weren't doing it like that. Hence, arrange to merge tuples from multiple update actions into a single transition table as much as we can. There is a problem, which is that if the firing of FK enforcement triggers and after-row triggers with transition tables is interspersed, we might need to report more tuples after some triggers have already seen the transition table. It seems like a bad idea for the transition table to be mutable between trigger calls. There's no good way around this without a major redesign of the FK logic, so for now, resolve it by opening a new transition table each time this happens. Also, ensure that AFTER STATEMENT triggers fire just once per statement, or once per transition table when we're forced to make more than one. Previous versions of Postgres have allowed each FK enforcement query to cause an additional firing of the AFTER STATEMENT triggers for the referencing table, but that's certainly not per spec. (We're still doing multiple firings of BEFORE STATEMENT triggers, though; is that something worth changing?) Also, forbid using transition tables with column-specific UPDATE triggers. The spec requires such transition tables to show only the tuples for which the UPDATE trigger would have fired, which means maintaining multiple transition tables or else somehow filtering the contents at readout. Maybe someday we'll bother to support that option, but it looks like a lot of trouble for a marginal feature. The transition tables are now managed by the AfterTriggers data structures, rather than being directly the responsibility of ModifyTable nodes. This removes a subtransaction-lifespan memory leak introduced by my previous band-aid patch 3c4359521. In passing, refactor the AfterTriggers data structures to reduce the management overhead for them, by using arrays of structs rather than several parallel arrays for per-query-level and per-subtransaction state. I failed to resist the temptation to do some copy-editing on the SGML docs about triggers, above and beyond merely documenting the effects of this patch. Back-patch to v10, because we don't want the semantics of transition tables to change post-release. Patch by me, with help and review from Thomas Munro. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-16 19:20:32 +02:00
NOTICE: trigger = self_ref_r_trig, old table = (1,), (2,1)
NOTICE: trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT
Fix SQL-spec incompatibilities in new transition table feature. The standard says that all changes of the same kind (insert, update, or delete) caused in one table by a single SQL statement should be reported in a single transition table; and by that, they mean to include foreign key enforcement actions cascading from the statement's direct effects. It's also reasonable to conclude that if the standard had wCTEs, they would say that effects of wCTEs applying to the same table as each other or the outer statement should be merged into one transition table. We weren't doing it like that. Hence, arrange to merge tuples from multiple update actions into a single transition table as much as we can. There is a problem, which is that if the firing of FK enforcement triggers and after-row triggers with transition tables is interspersed, we might need to report more tuples after some triggers have already seen the transition table. It seems like a bad idea for the transition table to be mutable between trigger calls. There's no good way around this without a major redesign of the FK logic, so for now, resolve it by opening a new transition table each time this happens. Also, ensure that AFTER STATEMENT triggers fire just once per statement, or once per transition table when we're forced to make more than one. Previous versions of Postgres have allowed each FK enforcement query to cause an additional firing of the AFTER STATEMENT triggers for the referencing table, but that's certainly not per spec. (We're still doing multiple firings of BEFORE STATEMENT triggers, though; is that something worth changing?) Also, forbid using transition tables with column-specific UPDATE triggers. The spec requires such transition tables to show only the tuples for which the UPDATE trigger would have fired, which means maintaining multiple transition tables or else somehow filtering the contents at readout. Maybe someday we'll bother to support that option, but it looks like a lot of trouble for a marginal feature. The transition tables are now managed by the AfterTriggers data structures, rather than being directly the responsibility of ModifyTable nodes. This removes a subtransaction-lifespan memory leak introduced by my previous band-aid patch 3c4359521. In passing, refactor the AfterTriggers data structures to reduce the management overhead for them, by using arrays of structs rather than several parallel arrays for per-query-level and per-subtransaction state. I failed to resist the temptation to do some copy-editing on the SGML docs about triggers, above and beyond merely documenting the effects of this patch. Back-patch to v10, because we don't want the semantics of transition tables to change post-release. Patch by me, with help and review from Thomas Munro. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-16 19:20:32 +02:00
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
Fix SQL-spec incompatibilities in new transition table feature. The standard says that all changes of the same kind (insert, update, or delete) caused in one table by a single SQL statement should be reported in a single transition table; and by that, they mean to include foreign key enforcement actions cascading from the statement's direct effects. It's also reasonable to conclude that if the standard had wCTEs, they would say that effects of wCTEs applying to the same table as each other or the outer statement should be merged into one transition table. We weren't doing it like that. Hence, arrange to merge tuples from multiple update actions into a single transition table as much as we can. There is a problem, which is that if the firing of FK enforcement triggers and after-row triggers with transition tables is interspersed, we might need to report more tuples after some triggers have already seen the transition table. It seems like a bad idea for the transition table to be mutable between trigger calls. There's no good way around this without a major redesign of the FK logic, so for now, resolve it by opening a new transition table each time this happens. Also, ensure that AFTER STATEMENT triggers fire just once per statement, or once per transition table when we're forced to make more than one. Previous versions of Postgres have allowed each FK enforcement query to cause an additional firing of the AFTER STATEMENT triggers for the referencing table, but that's certainly not per spec. (We're still doing multiple firings of BEFORE STATEMENT triggers, though; is that something worth changing?) Also, forbid using transition tables with column-specific UPDATE triggers. The spec requires such transition tables to show only the tuples for which the UPDATE trigger would have fired, which means maintaining multiple transition tables or else somehow filtering the contents at readout. Maybe someday we'll bother to support that option, but it looks like a lot of trouble for a marginal feature. The transition tables are now managed by the AfterTriggers data structures, rather than being directly the responsibility of ModifyTable nodes. This removes a subtransaction-lifespan memory leak introduced by my previous band-aid patch 3c4359521. In passing, refactor the AfterTriggers data structures to reduce the management overhead for them, by using arrays of structs rather than several parallel arrays for per-query-level and per-subtransaction state. I failed to resist the temptation to do some copy-editing on the SGML docs about triggers, above and beyond merely documenting the effects of this patch. Back-patch to v10, because we don't want the semantics of transition tables to change post-release. Patch by me, with help and review from Thomas Munro. Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
2017-09-16 19:20:32 +02:00
NOTICE: trigger = self_ref_s_trig, old table = (1,), (2,1), (3,2), (4,3)
drop table self_ref;
Add support for MERGE SQL command MERGE performs actions that modify rows in the target table using a source table or query. MERGE provides a single SQL statement that can conditionally INSERT/UPDATE/DELETE rows -- a task that would otherwise require multiple PL statements. For example, MERGE INTO target AS t USING source AS s ON t.tid = s.sid WHEN MATCHED AND t.balance > s.delta THEN UPDATE SET balance = t.balance - s.delta WHEN MATCHED THEN DELETE WHEN NOT MATCHED AND s.delta > 0 THEN INSERT VALUES (s.sid, s.delta) WHEN NOT MATCHED THEN DO NOTHING; MERGE works with regular tables, partitioned tables and inheritance hierarchies, including column and row security enforcement, as well as support for row and statement triggers and transition tables therein. MERGE is optimized for OLTP and is parameterizable, though also useful for large scale ETL/ELT. MERGE is not intended to be used in preference to existing single SQL commands for INSERT, UPDATE or DELETE since there is some overhead. MERGE can be used from PL/pgSQL. MERGE does not support targetting updatable views or foreign tables, and RETURNING clauses are not allowed either. These limitations are likely fixable with sufficient effort. Rewrite rules are also not supported, but it's not clear that we'd want to support them. Author: Pavan Deolasee <pavan.deolasee@gmail.com> Author: Álvaro Herrera <alvherre@alvh.no-ip.org> Author: Amit Langote <amitlangote09@gmail.com> Author: Simon Riggs <simon.riggs@enterprisedb.com> Reviewed-by: Peter Eisentraut <peter.eisentraut@enterprisedb.com> Reviewed-by: Andres Freund <andres@anarazel.de> (earlier versions) Reviewed-by: Peter Geoghegan <pg@bowt.ie> (earlier versions) Reviewed-by: Robert Haas <robertmhaas@gmail.com> (earlier versions) Reviewed-by: Japin Li <japinli@hotmail.com> Reviewed-by: Justin Pryzby <pryzby@telsasoft.com> Reviewed-by: Tomas Vondra <tomas.vondra@enterprisedb.com> Reviewed-by: Zhihong Yu <zyu@yugabyte.com> Discussion: https://postgr.es/m/CANP8+jKitBSrB7oTgT9CY2i1ObfOt36z0XMraQc+Xrz8QB0nXA@mail.gmail.com Discussion: https://postgr.es/m/CAH2-WzkJdBuxj9PO=2QaO9-3h3xGbQPZ34kJH=HukRekwM-GZg@mail.gmail.com Discussion: https://postgr.es/m/20201231134736.GA25392@alvherre.pgsql
2022-03-28 16:45:58 +02:00
--
-- 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');
Fix tupdesc lifespan bug with AfterTriggersTableData.storeslot. Commit 25936fd46 adjusted things so that the "storeslot" we use for remapping trigger tuples would have adequate lifespan, but it neglected to consider the lifespan of the tuple descriptor that the slot depends on. It turns out that in at least some cases, the tupdesc we are passing is a refcounted tupdesc, and the refcount for the slot's reference can get assigned to a resource owner having different lifespan than the slot does. That leads to an error like "tupdesc reference 0x7fdef236a1b8 is not owned by resource owner SubTransaction". Worse, because of a second oversight in the same commit, we'd try to free the same tupdesc refcount again while cleaning up after that error, leading to recursive errors and an "ERRORDATA_STACK_SIZE exceeded" PANIC. To fix the initial problem, let's just make a non-refcounted copy of the tupdesc we're supposed to use. That seems likely to guard against additional problems, since there's no strong reason for this code to assume that what it's given is a refcounted tupdesc; in which case there's an independent hazard of the tupdesc having shorter lifespan than the slot does. (I didn't bother trying to free said copy, since it should go away anyway when the (sub) transaction context is cleaned up.) The other issue can be fixed by making the code added to AfterTriggerFreeQuery work like the rest of that function, ie be sure that it doesn't try to free the same slot twice in the event of recursive error cleanup. While here, also clean up minor stylistic issues in the test case added by 25936fd46: don't use "create or replace function", as any name collision within the tests is likely to have ill effects that that won't mask; and don't use function names as generic as trigger_function1, especially if you're not going to drop them at the end of the test stanza. Per bug #17607 from Thomas Mc Kay. Back-patch to v12, as the previous fix was. Discussion: https://postgr.es/m/17607-bd8ccc81226f7f80@postgresql.org
2022-09-25 23:10:58 +02:00
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; $$;
Fix tupdesc lifespan bug with AfterTriggersTableData.storeslot. Commit 25936fd46 adjusted things so that the "storeslot" we use for remapping trigger tuples would have adequate lifespan, but it neglected to consider the lifespan of the tuple descriptor that the slot depends on. It turns out that in at least some cases, the tupdesc we are passing is a refcounted tupdesc, and the refcount for the slot's reference can get assigned to a resource owner having different lifespan than the slot does. That leads to an error like "tupdesc reference 0x7fdef236a1b8 is not owned by resource owner SubTransaction". Worse, because of a second oversight in the same commit, we'd try to free the same tupdesc refcount again while cleaning up after that error, leading to recursive errors and an "ERRORDATA_STACK_SIZE exceeded" PANIC. To fix the initial problem, let's just make a non-refcounted copy of the tupdesc we're supposed to use. That seems likely to guard against additional problems, since there's no strong reason for this code to assume that what it's given is a refcounted tupdesc; in which case there's an independent hazard of the tupdesc having shorter lifespan than the slot does. (I didn't bother trying to free said copy, since it should go away anyway when the (sub) transaction context is cleaned up.) The other issue can be fixed by making the code added to AfterTriggerFreeQuery work like the rest of that function, ie be sure that it doesn't try to free the same slot twice in the event of recursive error cleanup. While here, also clean up minor stylistic issues in the test case added by 25936fd46: don't use "create or replace function", as any name collision within the tests is likely to have ill effects that that won't mask; and don't use function names as generic as trigger_function1, especially if you're not going to drop them at the end of the test stanza. Per bug #17607 from Thomas Mc Kay. Back-patch to v12, as the previous fix was. Discussion: https://postgr.es/m/17607-bd8ccc81226f7f80@postgresql.org
2022-09-25 23:10:58 +02:00
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
Fix tupdesc lifespan bug with AfterTriggersTableData.storeslot. Commit 25936fd46 adjusted things so that the "storeslot" we use for remapping trigger tuples would have adequate lifespan, but it neglected to consider the lifespan of the tuple descriptor that the slot depends on. It turns out that in at least some cases, the tupdesc we are passing is a refcounted tupdesc, and the refcount for the slot's reference can get assigned to a resource owner having different lifespan than the slot does. That leads to an error like "tupdesc reference 0x7fdef236a1b8 is not owned by resource owner SubTransaction". Worse, because of a second oversight in the same commit, we'd try to free the same tupdesc refcount again while cleaning up after that error, leading to recursive errors and an "ERRORDATA_STACK_SIZE exceeded" PANIC. To fix the initial problem, let's just make a non-refcounted copy of the tupdesc we're supposed to use. That seems likely to guard against additional problems, since there's no strong reason for this code to assume that what it's given is a refcounted tupdesc; in which case there's an independent hazard of the tupdesc having shorter lifespan than the slot does. (I didn't bother trying to free said copy, since it should go away anyway when the (sub) transaction context is cleaned up.) The other issue can be fixed by making the code added to AfterTriggerFreeQuery work like the rest of that function, ie be sure that it doesn't try to free the same slot twice in the event of recursive error cleanup. While here, also clean up minor stylistic issues in the test case added by 25936fd46: don't use "create or replace function", as any name collision within the tests is likely to have ill effects that that won't mask; and don't use function names as generic as trigger_function1, especially if you're not going to drop them at the end of the test stanza. Per bug #17607 from Thomas Mc Kay. Back-patch to v12, as the previous fix was. Discussion: https://postgr.es/m/17607-bd8ccc81226f7f80@postgresql.org
2022-09-25 23:10:58 +02:00
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)
Fix tupdesc lifespan bug with AfterTriggersTableData.storeslot. Commit 25936fd46 adjusted things so that the "storeslot" we use for remapping trigger tuples would have adequate lifespan, but it neglected to consider the lifespan of the tuple descriptor that the slot depends on. It turns out that in at least some cases, the tupdesc we are passing is a refcounted tupdesc, and the refcount for the slot's reference can get assigned to a resource owner having different lifespan than the slot does. That leads to an error like "tupdesc reference 0x7fdef236a1b8 is not owned by resource owner SubTransaction". Worse, because of a second oversight in the same commit, we'd try to free the same tupdesc refcount again while cleaning up after that error, leading to recursive errors and an "ERRORDATA_STACK_SIZE exceeded" PANIC. To fix the initial problem, let's just make a non-refcounted copy of the tupdesc we're supposed to use. That seems likely to guard against additional problems, since there's no strong reason for this code to assume that what it's given is a refcounted tupdesc; in which case there's an independent hazard of the tupdesc having shorter lifespan than the slot does. (I didn't bother trying to free said copy, since it should go away anyway when the (sub) transaction context is cleaned up.) The other issue can be fixed by making the code added to AfterTriggerFreeQuery work like the rest of that function, ie be sure that it doesn't try to free the same slot twice in the event of recursive error cleanup. While here, also clean up minor stylistic issues in the test case added by 25936fd46: don't use "create or replace function", as any name collision within the tests is likely to have ill effects that that won't mask; and don't use function names as generic as trigger_function1, especially if you're not going to drop them at the end of the test stanza. Per bug #17607 from Thomas Mc Kay. Back-patch to v12, as the previous fix was. Discussion: https://postgr.es/m/17607-bd8ccc81226f7f80@postgresql.org
2022-09-25 23:10:58 +02:00
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
Fix tupdesc lifespan bug with AfterTriggersTableData.storeslot. Commit 25936fd46 adjusted things so that the "storeslot" we use for remapping trigger tuples would have adequate lifespan, but it neglected to consider the lifespan of the tuple descriptor that the slot depends on. It turns out that in at least some cases, the tupdesc we are passing is a refcounted tupdesc, and the refcount for the slot's reference can get assigned to a resource owner having different lifespan than the slot does. That leads to an error like "tupdesc reference 0x7fdef236a1b8 is not owned by resource owner SubTransaction". Worse, because of a second oversight in the same commit, we'd try to free the same tupdesc refcount again while cleaning up after that error, leading to recursive errors and an "ERRORDATA_STACK_SIZE exceeded" PANIC. To fix the initial problem, let's just make a non-refcounted copy of the tupdesc we're supposed to use. That seems likely to guard against additional problems, since there's no strong reason for this code to assume that what it's given is a refcounted tupdesc; in which case there's an independent hazard of the tupdesc having shorter lifespan than the slot does. (I didn't bother trying to free said copy, since it should go away anyway when the (sub) transaction context is cleaned up.) The other issue can be fixed by making the code added to AfterTriggerFreeQuery work like the rest of that function, ie be sure that it doesn't try to free the same slot twice in the event of recursive error cleanup. While here, also clean up minor stylistic issues in the test case added by 25936fd46: don't use "create or replace function", as any name collision within the tests is likely to have ill effects that that won't mask; and don't use function names as generic as trigger_function1, especially if you're not going to drop them at the end of the test stanza. Per bug #17607 from Thomas Mc Kay. Back-patch to v12, as the previous fix was. Discussion: https://postgr.es/m/17607-bd8ccc81226f7f80@postgresql.org
2022-09-25 23:10:58 +02:00
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
Fix tupdesc lifespan bug with AfterTriggersTableData.storeslot. Commit 25936fd46 adjusted things so that the "storeslot" we use for remapping trigger tuples would have adequate lifespan, but it neglected to consider the lifespan of the tuple descriptor that the slot depends on. It turns out that in at least some cases, the tupdesc we are passing is a refcounted tupdesc, and the refcount for the slot's reference can get assigned to a resource owner having different lifespan than the slot does. That leads to an error like "tupdesc reference 0x7fdef236a1b8 is not owned by resource owner SubTransaction". Worse, because of a second oversight in the same commit, we'd try to free the same tupdesc refcount again while cleaning up after that error, leading to recursive errors and an "ERRORDATA_STACK_SIZE exceeded" PANIC. To fix the initial problem, let's just make a non-refcounted copy of the tupdesc we're supposed to use. That seems likely to guard against additional problems, since there's no strong reason for this code to assume that what it's given is a refcounted tupdesc; in which case there's an independent hazard of the tupdesc having shorter lifespan than the slot does. (I didn't bother trying to free said copy, since it should go away anyway when the (sub) transaction context is cleaned up.) The other issue can be fixed by making the code added to AfterTriggerFreeQuery work like the rest of that function, ie be sure that it doesn't try to free the same slot twice in the event of recursive error cleanup. While here, also clean up minor stylistic issues in the test case added by 25936fd46: don't use "create or replace function", as any name collision within the tests is likely to have ill effects that that won't mask; and don't use function names as generic as trigger_function1, especially if you're not going to drop them at the end of the test stanza. Per bug #17607 from Thomas Mc Kay. Back-patch to v12, as the previous fix was. Discussion: https://postgr.es/m/17607-bd8ccc81226f7f80@postgresql.org
2022-09-25 23:10:58 +02:00
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;
Fix tupdesc lifespan bug with AfterTriggersTableData.storeslot. Commit 25936fd46 adjusted things so that the "storeslot" we use for remapping trigger tuples would have adequate lifespan, but it neglected to consider the lifespan of the tuple descriptor that the slot depends on. It turns out that in at least some cases, the tupdesc we are passing is a refcounted tupdesc, and the refcount for the slot's reference can get assigned to a resource owner having different lifespan than the slot does. That leads to an error like "tupdesc reference 0x7fdef236a1b8 is not owned by resource owner SubTransaction". Worse, because of a second oversight in the same commit, we'd try to free the same tupdesc refcount again while cleaning up after that error, leading to recursive errors and an "ERRORDATA_STACK_SIZE exceeded" PANIC. To fix the initial problem, let's just make a non-refcounted copy of the tupdesc we're supposed to use. That seems likely to guard against additional problems, since there's no strong reason for this code to assume that what it's given is a refcounted tupdesc; in which case there's an independent hazard of the tupdesc having shorter lifespan than the slot does. (I didn't bother trying to free said copy, since it should go away anyway when the (sub) transaction context is cleaned up.) The other issue can be fixed by making the code added to AfterTriggerFreeQuery work like the rest of that function, ie be sure that it doesn't try to free the same slot twice in the event of recursive error cleanup. While here, also clean up minor stylistic issues in the test case added by 25936fd46: don't use "create or replace function", as any name collision within the tests is likely to have ill effects that that won't mask; and don't use function names as generic as trigger_function1, especially if you're not going to drop them at the end of the test stanza. Per bug #17607 from Thomas Mc Kay. Back-patch to v12, as the previous fix was. Discussion: https://postgr.es/m/17607-bd8ccc81226f7f80@postgresql.org
2022-09-25 23:10:58 +02:00
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"
2022-09-25 00:38:35 +02:00
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();