diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 9a3d651..4390a8e 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -2496,3 +2496,322 @@ select * from rem1; 11 | bye remote (4 rows) +-- =================================================================== +-- test local triggers +-- =================================================================== +-- Trigger functions "borrowed" from triggers regress test. +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 trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger +LANGUAGE plpgsql AS $$ + +declare + oldnew text[]; + relid text; + argstr text; +begin + + relid := TG_relid::regclass; + argstr := ''; + 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 '%(%) % % % ON %', + tg_name, argstr, TG_when, TG_level, TG_OP, relid; + oldnew := '{}'::text[]; + if TG_OP != 'INSERT' then + oldnew := array_append(oldnew, format('OLD: %s', OLD)); + end if; + + if TG_OP != 'DELETE' then + oldnew := array_append(oldnew, format('NEW: %s', NEW)); + end if; + + RAISE NOTICE '%', array_to_string(oldnew, ','); + + if TG_OP = 'DELETE' then + return OLD; + else + return NEW; + end if; +end; +$$; +-- Test basic functionality +CREATE TRIGGER trig_row_before +BEFORE INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); +CREATE TRIGGER trig_row_after +AFTER INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); +delete from rem1; +NOTICE: trigger_func() called: action = DELETE, when = BEFORE, level = STATEMENT +NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1 +NOTICE: OLD: (1,hi) +NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1 +NOTICE: OLD: (10,"hi remote") +NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1 +NOTICE: OLD: (2,bye) +NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1 +NOTICE: OLD: (11,"bye remote") +NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1 +NOTICE: OLD: (1,hi) +NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1 +NOTICE: OLD: (10,"hi remote") +NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1 +NOTICE: OLD: (2,bye) +NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1 +NOTICE: OLD: (11,"bye remote") +NOTICE: trigger_func() called: action = DELETE, when = AFTER, level = STATEMENT +insert into rem1 values(1,'insert'); +NOTICE: trigger_func() called: action = INSERT, when = BEFORE, level = STATEMENT +NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1 +NOTICE: NEW: (1,insert) +NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1 +NOTICE: NEW: (1,insert) +NOTICE: trigger_func() called: action = INSERT, when = AFTER, level = STATEMENT +update rem1 set f2 = 'update' where f1 = 1; +NOTICE: trigger_func() called: action = UPDATE, when = BEFORE, level = STATEMENT +NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1 +NOTICE: OLD: (1,insert),NEW: (1,update) +NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1 +NOTICE: OLD: (1,insert),NEW: (1,update) +NOTICE: trigger_func() called: action = UPDATE, when = AFTER, level = STATEMENT +update rem1 set f2 = f2 || f2; +NOTICE: trigger_func() called: action = UPDATE, when = BEFORE, level = STATEMENT +NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1 +NOTICE: OLD: (1,update),NEW: (1,updateupdate) +NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1 +NOTICE: OLD: (1,update),NEW: (1,updateupdate) +NOTICE: trigger_func() called: action = UPDATE, when = AFTER, level = STATEMENT +-- cleanup +DROP TRIGGER trig_row_before ON rem1; +DROP TRIGGER trig_row_after ON rem1; +DROP TRIGGER trig_stmt_before ON rem1; +DROP TRIGGER trig_stmt_after ON rem1; +DELETE from rem1; +-- Test WHEN conditions +CREATE TRIGGER trig_row_before_insupd +BEFORE INSERT OR UPDATE ON rem1 +FOR EACH ROW +WHEN (NEW.f2 like '%update%') +EXECUTE PROCEDURE trigger_data(23,'skidoo'); +CREATE TRIGGER trig_row_after_insupd +AFTER INSERT OR UPDATE ON rem1 +FOR EACH ROW +WHEN (NEW.f2 like '%update%') +EXECUTE PROCEDURE trigger_data(23,'skidoo'); +-- Insert or update not matching: nothing happens +INSERT INTO rem1 values(1, 'insert'); +UPDATE rem1 set f2 = 'test'; +-- Insert or update not matching: triggers are fired +INSERT INTO rem1 values(2, 'update'); +NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW INSERT ON rem1 +NOTICE: NEW: (2,update) +NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW INSERT ON rem1 +NOTICE: NEW: (2,update) +UPDATE rem1 set f2 = 'update update' where f1 = '2'; +NOTICE: trig_row_before_insupd(23, skidoo) BEFORE ROW UPDATE ON rem1 +NOTICE: OLD: (2,update),NEW: (2,"update update") +NOTICE: trig_row_after_insupd(23, skidoo) AFTER ROW UPDATE ON rem1 +NOTICE: OLD: (2,update),NEW: (2,"update update") +CREATE TRIGGER trig_row_before_delete +BEFORE DELETE ON rem1 +FOR EACH ROW +WHEN (OLD.f2 like '%update%') +EXECUTE PROCEDURE trigger_data(23,'skidoo'); +CREATE TRIGGER trig_row_after_delete +AFTER DELETE ON rem1 +FOR EACH ROW +WHEN (OLD.f2 like '%update%') +EXECUTE PROCEDURE trigger_data(23,'skidoo'); +-- Trigger is fired for f1=2, not for f1=1 +DELETE FROM rem1; +NOTICE: trig_row_before_delete(23, skidoo) BEFORE ROW DELETE ON rem1 +NOTICE: OLD: (2,"update update") +NOTICE: trig_row_after_delete(23, skidoo) AFTER ROW DELETE ON rem1 +NOTICE: OLD: (2,"update update") +-- cleanup +DROP TRIGGER trig_row_before_insupd ON rem1; +DROP TRIGGER trig_row_after_insupd ON rem1; +DROP TRIGGER trig_row_before_delete ON rem1; +DROP TRIGGER trig_row_after_delete ON rem1; +-- Test various RETURN statements in BEFORE triggers. +CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$ + BEGIN + NEW.f2 := NEW.f2 || ' triggered !'; + RETURN NEW; + END +$$ language plpgsql; +CREATE TRIGGER trig_row_before_insupd +BEFORE INSERT OR UPDATE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate(); +-- The new values should have 'triggered' appended +INSERT INTO rem1 values(1, 'insert'); +SELECT * from loc1; + f1 | f2 +----+-------------------- + 1 | insert triggered ! +(1 row) + +INSERT INTO rem1 values(2, 'insert') RETURNING f2; + f2 +-------------------- + insert triggered ! +(1 row) + +SELECT * from loc1; + f1 | f2 +----+-------------------- + 1 | insert triggered ! + 2 | insert triggered ! +(2 rows) + +UPDATE rem1 set f2 = ''; +SELECT * from loc1; + f1 | f2 +----+-------------- + 1 | triggered ! + 2 | triggered ! +(2 rows) + +UPDATE rem1 set f2 = 'skidoo' RETURNING f2; + f2 +-------------------- + skidoo triggered ! + skidoo triggered ! +(2 rows) + +SELECT * from loc1; + f1 | f2 +----+-------------------- + 1 | skidoo triggered ! + 2 | skidoo triggered ! +(2 rows) + +DELETE FROM rem1; +-- Add a second trigger, to check that the changes are propagated correctly +-- from trigger to trigger +CREATE TRIGGER trig_row_before_insupd2 +BEFORE INSERT OR UPDATE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate(); +INSERT INTO rem1 values(1, 'insert'); +SELECT * from loc1; + f1 | f2 +----+-------------------------------- + 1 | insert triggered ! triggered ! +(1 row) + +INSERT INTO rem1 values(2, 'insert') RETURNING f2; + f2 +-------------------------------- + insert triggered ! triggered ! +(1 row) + +SELECT * from loc1; + f1 | f2 +----+-------------------------------- + 1 | insert triggered ! triggered ! + 2 | insert triggered ! triggered ! +(2 rows) + +UPDATE rem1 set f2 = ''; +SELECT * from loc1; + f1 | f2 +----+-------------------------- + 1 | triggered ! triggered ! + 2 | triggered ! triggered ! +(2 rows) + +UPDATE rem1 set f2 = 'skidoo' RETURNING f2; + f2 +-------------------------------- + skidoo triggered ! triggered ! + skidoo triggered ! triggered ! +(2 rows) + +SELECT * from loc1; + f1 | f2 +----+-------------------------------- + 1 | skidoo triggered ! triggered ! + 2 | skidoo triggered ! triggered ! +(2 rows) + +DROP TRIGGER trig_row_before_insupd ON rem1; +DROP TRIGGER trig_row_before_insupd2 ON rem1; +DELETE from rem1; +INSERT INTO rem1 VALUES (1, 'test'); +-- Test with a trigger returning NULL +CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$ + BEGIN + RETURN NULL; + END +$$ language plpgsql; +CREATE TRIGGER trig_null +BEFORE INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trig_null(); +-- Nothing should have changed. +INSERT INTO rem1 VALUES (2, 'test2'); +SELECT * from loc1; + f1 | f2 +----+------ + 1 | test +(1 row) + +UPDATE rem1 SET f2 = 'test2'; +SELECT * from loc1; + f1 | f2 +----+------ + 1 | test +(1 row) + +DELETE from rem1; +SELECT * from loc1; + f1 | f2 +----+------ + 1 | test +(1 row) + +DROP TRIGGER trig_null ON rem1; +DELETE from rem1; +-- Test a combination of local and remote triggers +CREATE TRIGGER trig_row_before +BEFORE INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); +CREATE TRIGGER trig_row_after +AFTER INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); +CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1 +FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate(); +INSERT INTO rem1(f2) VALUES ('test'); +NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1 +NOTICE: NEW: (12,test) +NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1 +NOTICE: NEW: (12,"test triggered !") +UPDATE rem1 SET f2 = 'testo'; +NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1 +NOTICE: OLD: (12,"test triggered !"),NEW: (12,testo) +NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1 +NOTICE: OLD: (12,"test triggered !"),NEW: (12,"testo triggered !") +-- Test returning system attributes +INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax; +NOTICE: trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1 +NOTICE: NEW: (13,test) +NOTICE: trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1 +NOTICE: NEW: (13,"test triggered !") + ctid | xmin | xmax +--------+------+------------ + (0,27) | 180 | 4294967295 +(1 row) + diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 21b15ca..3d2ee73 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -390,3 +390,219 @@ insert into loc1(f2) values('bye'); insert into rem1(f2) values('bye remote'); select * from loc1; select * from rem1; + +-- =================================================================== +-- test local triggers +-- =================================================================== + +-- Trigger functions "borrowed" from triggers regress test. +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 trig_stmt_before BEFORE DELETE OR INSERT OR UPDATE ON rem1 + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER trig_stmt_after AFTER DELETE OR INSERT OR UPDATE ON rem1 + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); + +CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger +LANGUAGE plpgsql AS $$ + +declare + oldnew text[]; + relid text; + argstr text; +begin + + relid := TG_relid::regclass; + argstr := ''; + 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 '%(%) % % % ON %', + tg_name, argstr, TG_when, TG_level, TG_OP, relid; + oldnew := '{}'::text[]; + if TG_OP != 'INSERT' then + oldnew := array_append(oldnew, format('OLD: %s', OLD)); + end if; + + if TG_OP != 'DELETE' then + oldnew := array_append(oldnew, format('NEW: %s', NEW)); + end if; + + RAISE NOTICE '%', array_to_string(oldnew, ','); + + if TG_OP = 'DELETE' then + return OLD; + else + return NEW; + end if; +end; +$$; + +-- Test basic functionality +CREATE TRIGGER trig_row_before +BEFORE INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + +CREATE TRIGGER trig_row_after +AFTER INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + +delete from rem1; +insert into rem1 values(1,'insert'); +update rem1 set f2 = 'update' where f1 = 1; +update rem1 set f2 = f2 || f2; + + +-- cleanup +DROP TRIGGER trig_row_before ON rem1; +DROP TRIGGER trig_row_after ON rem1; +DROP TRIGGER trig_stmt_before ON rem1; +DROP TRIGGER trig_stmt_after ON rem1; + +DELETE from rem1; + + +-- Test WHEN conditions + +CREATE TRIGGER trig_row_before_insupd +BEFORE INSERT OR UPDATE ON rem1 +FOR EACH ROW +WHEN (NEW.f2 like '%update%') +EXECUTE PROCEDURE trigger_data(23,'skidoo'); + +CREATE TRIGGER trig_row_after_insupd +AFTER INSERT OR UPDATE ON rem1 +FOR EACH ROW +WHEN (NEW.f2 like '%update%') +EXECUTE PROCEDURE trigger_data(23,'skidoo'); + +-- Insert or update not matching: nothing happens +INSERT INTO rem1 values(1, 'insert'); +UPDATE rem1 set f2 = 'test'; + +-- Insert or update not matching: triggers are fired +INSERT INTO rem1 values(2, 'update'); +UPDATE rem1 set f2 = 'update update' where f1 = '2'; + +CREATE TRIGGER trig_row_before_delete +BEFORE DELETE ON rem1 +FOR EACH ROW +WHEN (OLD.f2 like '%update%') +EXECUTE PROCEDURE trigger_data(23,'skidoo'); + +CREATE TRIGGER trig_row_after_delete +AFTER DELETE ON rem1 +FOR EACH ROW +WHEN (OLD.f2 like '%update%') +EXECUTE PROCEDURE trigger_data(23,'skidoo'); + +-- Trigger is fired for f1=2, not for f1=1 +DELETE FROM rem1; + +-- cleanup +DROP TRIGGER trig_row_before_insupd ON rem1; +DROP TRIGGER trig_row_after_insupd ON rem1; +DROP TRIGGER trig_row_before_delete ON rem1; +DROP TRIGGER trig_row_after_delete ON rem1; + + +-- Test various RETURN statements in BEFORE triggers. + +CREATE FUNCTION trig_row_before_insupdate() RETURNS TRIGGER AS $$ + BEGIN + NEW.f2 := NEW.f2 || ' triggered !'; + RETURN NEW; + END +$$ language plpgsql; + +CREATE TRIGGER trig_row_before_insupd +BEFORE INSERT OR UPDATE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate(); + +-- The new values should have 'triggered' appended +INSERT INTO rem1 values(1, 'insert'); +SELECT * from loc1; +INSERT INTO rem1 values(2, 'insert') RETURNING f2; +SELECT * from loc1; +UPDATE rem1 set f2 = ''; +SELECT * from loc1; +UPDATE rem1 set f2 = 'skidoo' RETURNING f2; +SELECT * from loc1; + +DELETE FROM rem1; + +-- Add a second trigger, to check that the changes are propagated correctly +-- from trigger to trigger +CREATE TRIGGER trig_row_before_insupd2 +BEFORE INSERT OR UPDATE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate(); + +INSERT INTO rem1 values(1, 'insert'); +SELECT * from loc1; +INSERT INTO rem1 values(2, 'insert') RETURNING f2; +SELECT * from loc1; +UPDATE rem1 set f2 = ''; +SELECT * from loc1; +UPDATE rem1 set f2 = 'skidoo' RETURNING f2; +SELECT * from loc1; + +DROP TRIGGER trig_row_before_insupd ON rem1; +DROP TRIGGER trig_row_before_insupd2 ON rem1; + +DELETE from rem1; + +INSERT INTO rem1 VALUES (1, 'test'); + +-- Test with a trigger returning NULL +CREATE FUNCTION trig_null() RETURNS TRIGGER AS $$ + BEGIN + RETURN NULL; + END +$$ language plpgsql; + +CREATE TRIGGER trig_null +BEFORE INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trig_null(); + +-- Nothing should have changed. +INSERT INTO rem1 VALUES (2, 'test2'); + +SELECT * from loc1; + +UPDATE rem1 SET f2 = 'test2'; + +SELECT * from loc1; + +DELETE from rem1; + +SELECT * from loc1; + +DROP TRIGGER trig_null ON rem1; +DELETE from rem1; + +-- Test a combination of local and remote triggers +CREATE TRIGGER trig_row_before +BEFORE INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + +CREATE TRIGGER trig_row_after +AFTER INSERT OR UPDATE OR DELETE ON rem1 +FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + +CREATE TRIGGER trig_local_before BEFORE INSERT OR UPDATE ON loc1 +FOR EACH ROW EXECUTE PROCEDURE trig_row_before_insupdate(); + +INSERT INTO rem1(f2) VALUES ('test'); +UPDATE rem1 SET f2 = 'testo'; + +-- Test returning system attributes +INSERT INTO rem1(f2) VALUES ('test') RETURNING ctid, xmin, xmax; diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml index 6c06f1a..abaaa6b 100644 --- a/doc/src/sgml/fdwhandler.sgml +++ b/doc/src/sgml/fdwhandler.sgml @@ -308,7 +308,8 @@ AddForeignUpdateTargets (Query *parsetree, extra values to be fetched. Each such entry must be marked resjunk = true, and must have a distinct resname that will identify it at execution time. - Avoid using names matching ctidN or + Avoid using names matching ctidN, + wholerow, or wholerowN, as the core system can generate junk columns of these names. diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml index a8fba49..d270d66 100644 --- a/doc/src/sgml/ref/create_trigger.sgml +++ b/doc/src/sgml/ref/create_trigger.sgml @@ -43,9 +43,10 @@ CREATE [ CONSTRAINT ] TRIGGER name CREATE TRIGGER creates a new trigger. The - trigger will be associated with the specified table or view and will - execute the specified function function_name when certain events occur. + trigger will be associated with the specified table, view, or foreign table + and will execute the specified + function function_name when + certain events occur. @@ -93,7 +94,7 @@ CREATE [ CONSTRAINT ] TRIGGER name The following table summarizes which types of triggers may be used on - tables and views: + tables, views, and foreign tables: @@ -110,8 +111,8 @@ CREATE [ CONSTRAINT ] TRIGGER name BEFORE INSERT/UPDATE/DELETE - Tables - Tables and views + Tables and foreign tables + Tables, views, and foreign tables TRUNCATE @@ -121,8 +122,8 @@ CREATE [ CONSTRAINT ] TRIGGER name AFTER INSERT/UPDATE/DELETE - Tables - Tables and views + Tables and foreign tables + Tables, views, and foreign tables TRUNCATE @@ -164,13 +165,13 @@ CREATE [ CONSTRAINT ] TRIGGER name constraint trigger. This is the same as a regular trigger except that the timing of the trigger firing can be adjusted using . - Constraint triggers must be AFTER ROW triggers. They can - be fired either at the end of the statement causing the triggering event, - or at the end of the containing transaction; in the latter case they are - said to be deferred. A pending deferred-trigger firing can - also be forced to happen immediately by using SET CONSTRAINTS. - Constraint triggers are expected to raise an exception when the constraints - they implement are violated. + Constraint triggers must be AFTER ROW triggers on tables. They + can be fired either at the end of the statement causing the triggering + event, or at the end of the containing transaction; in the latter case they + are said to be deferred. A pending deferred-trigger firing + can also be forced to happen immediately by using SET + CONSTRAINTS. Constraint triggers are expected to raise an exception + when the constraints they implement are violated. @@ -244,8 +245,8 @@ UPDATE OF column_name1 [, column_name2table_name - The name (optionally schema-qualified) of the table or view the trigger - is for. + The name (optionally schema-qualified) of the table, view, or foreign + table the trigger is for. @@ -481,6 +482,14 @@ CREATE TRIGGER view_insert Compatibility + + The CREATE TRIGGER statement in PostgreSQL implements a subset of the diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml index f579340..7ed91dd 100644 --- a/doc/src/sgml/trigger.sgml +++ b/doc/src/sgml/trigger.sgml @@ -33,20 +33,21 @@ A trigger is a specification that the database should automatically execute a particular function whenever a certain type of operation is - performed. Triggers can be attached to both tables and views. + performed. Triggers can be attached to tables, views, and foreign tables. - On tables, triggers can be defined to execute either before or after any - INSERT, UPDATE, or - DELETE operation, either once per modified row, + On tables and foreign tables, triggers can be defined to execute either + before or after any INSERT, UPDATE, + or DELETE operation, either once per modified row, or once per SQL statement. UPDATE triggers can moreover be set to fire only if certain columns are mentioned in the SET clause of the UPDATE statement. Triggers can also fire for TRUNCATE statements. If a trigger event occurs, the trigger's function is called at the - appropriate time to handle the event. + appropriate time to handle the event. Foreign tables do not support the + TRUNCATE statement at all. @@ -111,10 +112,10 @@ triggers fire immediately before a particular row is operated on, while row-level AFTER triggers fire at the end of the statement (but before any statement-level AFTER triggers). - These types of triggers may only be defined on tables. Row-level - INSTEAD OF triggers may only be defined on views, and fire - immediately as each row in the view is identified as needing to be - operated on. + These types of triggers may only be defined on tables and foreign tables. + Row-level INSTEAD OF triggers may only be defined on views, + and fire immediately as each row in the view is identified as needing to + be operated on. diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 25f01e5..7f3f730 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -3180,6 +3180,9 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_DisableTrig: /* DISABLE TRIGGER variants */ case AT_DisableTrigAll: case AT_DisableTrigUser: + ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE); + pass = AT_PASS_MISC; + break; case AT_EnableRule: /* ENABLE/DISABLE RULE variants */ case AT_EnableAlwaysRule: case AT_EnableReplicaRule: diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index fa74bd2..f4e4bfb 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -56,6 +56,7 @@ #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/tqual.h" +#include "utils/tuplestore.h" /* GUC variables */ @@ -195,6 +196,30 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, RelationGetRelationName(rel)), errdetail("Views cannot have TRUNCATE triggers."))); } + else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + { + if (stmt->timing != TRIGGER_TYPE_BEFORE && + stmt->timing != TRIGGER_TYPE_AFTER) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a foreign table", + RelationGetRelationName(rel)), + errdetail("Foreign tables cannot have INSTEAD OF triggers."))); + + if (TRIGGER_FOR_TRUNCATE(stmt->events)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a foreign table", + RelationGetRelationName(rel)), + errdetail("Foreign tables cannot have TRUNCATE triggers."))); + + if (stmt->isconstraint) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a foreign table", + RelationGetRelationName(rel)), + errdetail("Foreign tables cannot have constraint triggers."))); + } else ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -1080,10 +1105,11 @@ RemoveTriggerById(Oid trigOid) rel = heap_open(relid, AccessExclusiveLock); if (rel->rd_rel->relkind != RELKIND_RELATION && - rel->rd_rel->relkind != RELKIND_VIEW) + rel->rd_rel->relkind != RELKIND_VIEW && + rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table or view", + errmsg("\"%s\" is not a table, view, or foreign table", RelationGetRelationName(rel)))); if (!allowSystemTableMods && IsSystemRelation(rel)) @@ -1184,10 +1210,12 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid, form = (Form_pg_class) GETSTRUCT(tuple); /* only tables and views can have triggers */ - if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW) + if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW && + form->relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table or view", rv->relname))); + errmsg("\"%s\" is not a table, view, or foreign table", + rv->relname))); /* you must own the table to rename one of its triggers */ if (!pg_class_ownercheck(relid, GetUserId())) @@ -2164,7 +2192,8 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo) bool ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, - ItemPointer tupleid) + ItemPointer tupleid, + HeapTuple fdw_trigtuple) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; bool result = true; @@ -2174,10 +2203,16 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, TupleTableSlot *newSlot; int i; - trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid, - LockTupleExclusive, &newSlot); - if (trigtuple == NULL) - return false; + Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid)); + if (fdw_trigtuple == NULL) + { + trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid, + LockTupleExclusive, &newSlot); + if (trigtuple == NULL) + return false; + } + else + trigtuple = fdw_trigtuple; LocTriggerData.type = T_TriggerData; LocTriggerData.tg_event = TRIGGER_EVENT_DELETE | @@ -2215,29 +2250,38 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, if (newtuple != trigtuple) heap_freetuple(newtuple); } - heap_freetuple(trigtuple); + if (trigtuple != fdw_trigtuple) + heap_freetuple(trigtuple); return result; } void ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, - ItemPointer tupleid) + ItemPointer tupleid, + HeapTuple fdw_trigtuple) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; if (trigdesc && trigdesc->trig_delete_after_row) { - HeapTuple trigtuple = GetTupleForTrigger(estate, - NULL, - relinfo, - tupleid, - LockTupleExclusive, - NULL); + HeapTuple trigtuple; + + Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid)); + if (fdw_trigtuple == NULL) + trigtuple = GetTupleForTrigger(estate, + NULL, + relinfo, + tupleid, + LockTupleExclusive, + NULL); + else + trigtuple = fdw_trigtuple; AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE, true, trigtuple, NULL, NIL, NULL); - heap_freetuple(trigtuple); + if (trigtuple != fdw_trigtuple) + heap_freetuple(trigtuple); } } @@ -2353,7 +2397,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo) TupleTableSlot * ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, - ItemPointer tupleid, TupleTableSlot *slot) + ItemPointer tupleid, + HeapTuple fdw_trigtuple, + TupleTableSlot *slot) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; HeapTuple slottuple = ExecMaterializeSlot(slot); @@ -2380,11 +2426,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, else lockmode = LockTupleNoKeyExclusive; - /* get a copy of the on-disk tuple we are planning to update */ - trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid, - lockmode, &newSlot); - if (trigtuple == NULL) - return NULL; /* cancel the update action */ + Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid)); + if (fdw_trigtuple == NULL) + { + /* get a copy of the on-disk tuple we are planning to update */ + trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid, + lockmode, &newSlot); + if (trigtuple == NULL) + return NULL; /* cancel the update action */ + } + else + { + trigtuple = fdw_trigtuple; + newSlot = NULL; + } /* * In READ COMMITTED isolation level it's possible that target tuple was @@ -2437,11 +2492,13 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, heap_freetuple(oldtuple); if (newtuple == NULL) { - heap_freetuple(trigtuple); + if (trigtuple != fdw_trigtuple) + heap_freetuple(trigtuple); return NULL; /* "do nothing" */ } } - heap_freetuple(trigtuple); + if (trigtuple != fdw_trigtuple) + heap_freetuple(trigtuple); if (newtuple != slottuple) { @@ -2465,23 +2522,31 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, void ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, HeapTuple newtuple, + HeapTuple fdw_trigtuple, List *recheckIndexes) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; if (trigdesc && trigdesc->trig_update_after_row) { - HeapTuple trigtuple = GetTupleForTrigger(estate, - NULL, - relinfo, - tupleid, - LockTupleExclusive, - NULL); + HeapTuple trigtuple; + + Assert(HeapTupleIsValid(fdw_trigtuple) ^ ItemPointerIsValid(tupleid)); + if (fdw_trigtuple == NULL) + trigtuple = GetTupleForTrigger(estate, + NULL, + relinfo, + tupleid, + LockTupleExclusive, + NULL); + else + trigtuple = fdw_trigtuple; AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE, true, trigtuple, newtuple, recheckIndexes, GetModifiedColumns(relinfo, estate)); - heap_freetuple(trigtuple); + if (trigtuple != fdw_trigtuple) + heap_freetuple(trigtuple); } } @@ -2942,13 +3007,22 @@ typedef SetConstraintStateData *SetConstraintState; * Per-trigger-event data * * The actual per-event data, AfterTriggerEventData, includes DONE/IN_PROGRESS - * status bits and one or two tuple CTIDs. Each event record also has an - * associated AfterTriggerSharedData that is shared across all instances - * of similar events within a "chunk". + * status bits and up to two tuple CTIDs. Each event record also has an + * associated AfterTriggerSharedData that is shared across all instances of + * similar events within a "chunk". * - * We arrange not to waste storage on ate_ctid2 for non-update events. - * We could go further and not store either ctid for statement-level triggers, - * but that seems unlikely to be worth the trouble. + * For row-level triggers, we arrange not to waste storage on unneeded ctid + * fields. Updates of regular tables use two; inserts and deletes of regular + * tables use one; foreign tables always use zero and save the tuple(s) to a + * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to + * retrieve a fresh tuple or pair of tuples from that tuplestore, while + * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved + * tuple(s). This permits storing tuples once regardless of the number of + * row-level triggers on a foreign table. + * + * Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they + * require no ctid field. We lack the flag bit space to neatly represent that + * distinct case, and it seems unlikely to be worth much trouble. * * Note: ats_firing_id is initially zero and is set to something else when * AFTER_TRIGGER_IN_PROGRESS is set. It indicates which trigger firing @@ -2963,9 +3037,14 @@ typedef uint32 TriggerFlags; #define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order * bits */ -#define AFTER_TRIGGER_2CTIDS 0x10000000 -#define AFTER_TRIGGER_DONE 0x20000000 -#define AFTER_TRIGGER_IN_PROGRESS 0x40000000 +#define AFTER_TRIGGER_DONE 0x10000000 +#define AFTER_TRIGGER_IN_PROGRESS 0x20000000 +/* bits describing the size and tuple sources of this event */ +#define AFTER_TRIGGER_FDW_REUSE 0x00000000 +#define AFTER_TRIGGER_FDW_FETCH 0x80000000 +#define AFTER_TRIGGER_1CTID 0x40000000 +#define AFTER_TRIGGER_2CTID 0xC0000000 +#define AFTER_TRIGGER_TUP_BITS 0xC0000000 typedef struct AfterTriggerSharedData *AfterTriggerShared; @@ -2986,16 +3065,25 @@ typedef struct AfterTriggerEventData ItemPointerData ate_ctid2; /* new updated tuple */ } AfterTriggerEventData; -/* This struct must exactly match the one above except for not having ctid2 */ +/* AfterTriggerEventData, minus ate_ctid2 */ typedef struct AfterTriggerEventDataOneCtid { TriggerFlags ate_flags; /* status bits and offset to shared data */ ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */ } AfterTriggerEventDataOneCtid; +/* AfterTriggerEventData, minus ate_ctid1 and ate_ctid2 */ +typedef struct AfterTriggerEventDataZeroCtids +{ + TriggerFlags ate_flags; /* status bits and offset to shared data */ +} AfterTriggerEventDataZeroCtids; + #define SizeofTriggerEvent(evt) \ - (((evt)->ate_flags & AFTER_TRIGGER_2CTIDS) ? \ - sizeof(AfterTriggerEventData) : sizeof(AfterTriggerEventDataOneCtid)) + (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \ + sizeof(AfterTriggerEventData) : \ + ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \ + sizeof(AfterTriggerEventDataOneCtid) : \ + sizeof(AfterTriggerEventDataZeroCtids)) #define GetTriggerSharedData(evt) \ ((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET))) @@ -3068,7 +3156,11 @@ typedef struct AfterTriggerEventList * immediate-mode triggers, and append any deferred events to the main events * list. * - * maxquerydepth is just the allocated length of query_stack. + * fdw_tuplestores[query_depth] is a tuplestore containing the foreign tuples + * needed for the current query. + * + * maxquerydepth is just the allocated length of query_stack and + * fdw_tuplestores. * * state_stack is a stack of pointers to saved copies of the SET CONSTRAINTS * state data; each subtransaction level that modifies that state first @@ -3097,6 +3189,7 @@ typedef struct AfterTriggersData AfterTriggerEventList events; /* deferred-event list */ int query_depth; /* current query list index */ AfterTriggerEventList *query_stack; /* events pending from each query */ + Tuplestorestate **fdw_tuplestores; /* foreign tuples from each query */ int maxquerydepth; /* allocated len of above array */ MemoryContext event_cxt; /* memory context for events, if any */ @@ -3113,18 +3206,60 @@ typedef AfterTriggersData *AfterTriggers; static AfterTriggers afterTriggers; - static void AfterTriggerExecute(AfterTriggerEvent event, Relation rel, TriggerDesc *trigdesc, FmgrInfo *finfo, Instrumentation *instr, - MemoryContext per_tuple_context); + MemoryContext per_tuple_context, + TupleTableSlot *trig_tuple_slot1, + TupleTableSlot *trig_tuple_slot2); static SetConstraintState SetConstraintStateCreate(int numalloc); static SetConstraintState SetConstraintStateCopy(SetConstraintState state); static SetConstraintState SetConstraintStateAddItem(SetConstraintState state, Oid tgoid, bool tgisdeferred); +/* + * Gets the current query fdw tuplestore and initializes it if necessary + */ +static Tuplestorestate * +GetCurrentFDWTuplestore() +{ + Tuplestorestate *ret; + + ret = afterTriggers->fdw_tuplestores[afterTriggers->query_depth]; + if (ret == NULL) + { + MemoryContext oldcxt; + ResourceOwner saveResourceOwner; + + /* + * Make the tuplestore valid until end of transaction. This is the + * allocation lifespan of the associated events list, but we really + * only need it until AfterTriggerEndQuery(). + */ + oldcxt = MemoryContextSwitchTo(TopTransactionContext); + saveResourceOwner = CurrentResourceOwner; + PG_TRY(); + { + CurrentResourceOwner = TopTransactionResourceOwner; + ret = tuplestore_begin_heap(false, false, work_mem); + } + PG_CATCH(); + { + CurrentResourceOwner = saveResourceOwner; + PG_RE_THROW(); + } + PG_END_TRY(); + CurrentResourceOwner = saveResourceOwner; + MemoryContextSwitchTo(oldcxt); + + afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = ret; + } + + return ret; +} + /* ---------- * afterTriggerCheckState() * @@ -3365,13 +3500,17 @@ afterTriggerRestoreEventList(AfterTriggerEventList *events, * instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger), * or NULL if no instrumentation is wanted. * per_tuple_context: memory context to call trigger function in. + * trig_tuple_slot1: scratch slot for tg_trigtuple (foreign tables only) + * trig_tuple_slot2: scratch slot for tg_newtuple (foreign tables only) * ---------- */ static void AfterTriggerExecute(AfterTriggerEvent event, Relation rel, TriggerDesc *trigdesc, FmgrInfo *finfo, Instrumentation *instr, - MemoryContext per_tuple_context) + MemoryContext per_tuple_context, + TupleTableSlot *trig_tuple_slot1, + TupleTableSlot *trig_tuple_slot2) { AfterTriggerShared evtshared = GetTriggerSharedData(event); Oid tgoid = evtshared->ats_tgoid; @@ -3408,34 +3547,81 @@ AfterTriggerExecute(AfterTriggerEvent event, /* * Fetch the required tuple(s). */ - if (ItemPointerIsValid(&(event->ate_ctid1))) + switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS) { - ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self)); - if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL)) - elog(ERROR, "failed to fetch tuple1 for AFTER trigger"); - LocTriggerData.tg_trigtuple = &tuple1; - LocTriggerData.tg_trigtuplebuf = buffer1; - } - else - { - LocTriggerData.tg_trigtuple = NULL; - LocTriggerData.tg_trigtuplebuf = InvalidBuffer; - } + case AFTER_TRIGGER_FDW_FETCH: + { + Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore(); - /* don't touch ctid2 if not there */ - if ((event->ate_flags & AFTER_TRIGGER_2CTIDS) && - ItemPointerIsValid(&(event->ate_ctid2))) - { - ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self)); - if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL)) - elog(ERROR, "failed to fetch tuple2 for AFTER trigger"); - LocTriggerData.tg_newtuple = &tuple2; - LocTriggerData.tg_newtuplebuf = buffer2; - } - else - { - LocTriggerData.tg_newtuple = NULL; - LocTriggerData.tg_newtuplebuf = InvalidBuffer; + if (!tuplestore_gettupleslot(fdw_tuplestore, true, false, + trig_tuple_slot1)) + elog(ERROR, "failed to fetch tuple1 for AFTER trigger"); + + if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) == + TRIGGER_EVENT_UPDATE && + !tuplestore_gettupleslot(fdw_tuplestore, true, false, + trig_tuple_slot2)) + elog(ERROR, "failed to fetch tuple2 for AFTER trigger"); + } + /* fall through */ + case AFTER_TRIGGER_FDW_REUSE: + /* + * Using ExecMaterializeSlot() rather than ExecFetchSlotTuple() + * ensures that tg_trigtuple does not reference tuplestore memory. + * (It is formally possible for the trigger function to queue + * trigger events that add to the same tuplestore, which can push + * other tuples out of memory.) The distinction is academic, + * because we start with a minimal tuple that ExecFetchSlotTuple() + * must materialize anyway. + */ + LocTriggerData.tg_trigtuple = + ExecMaterializeSlot(trig_tuple_slot1); + LocTriggerData.tg_trigtuplebuf = InvalidBuffer; + + if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) == + TRIGGER_EVENT_UPDATE) + { + LocTriggerData.tg_newtuple = + ExecMaterializeSlot(trig_tuple_slot2); + LocTriggerData.tg_newtuplebuf = InvalidBuffer; + } + else + LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_newtuplebuf = InvalidBuffer; + + break; + + default: + if (ItemPointerIsValid(&(event->ate_ctid1))) + { + ItemPointerCopy(&(event->ate_ctid1), &(tuple1.t_self)); + if (!heap_fetch(rel, SnapshotAny, &tuple1, &buffer1, false, NULL)) + elog(ERROR, "failed to fetch tuple1 for AFTER trigger"); + LocTriggerData.tg_trigtuple = &tuple1; + LocTriggerData.tg_trigtuplebuf = buffer1; + } + else + { + LocTriggerData.tg_trigtuple = NULL; + LocTriggerData.tg_trigtuplebuf = InvalidBuffer; + } + + /* don't touch ctid2 if not there */ + if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) == + AFTER_TRIGGER_2CTID && + ItemPointerIsValid(&(event->ate_ctid2))) + { + ItemPointerCopy(&(event->ate_ctid2), &(tuple2.t_self)); + if (!heap_fetch(rel, SnapshotAny, &tuple2, &buffer2, false, NULL)) + elog(ERROR, "failed to fetch tuple2 for AFTER trigger"); + LocTriggerData.tg_newtuple = &tuple2; + LocTriggerData.tg_newtuplebuf = buffer2; + } + else + { + LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_newtuplebuf = InvalidBuffer; + } } /* @@ -3457,7 +3643,9 @@ AfterTriggerExecute(AfterTriggerEvent event, finfo, NULL, per_tuple_context); - if (rettuple != NULL && rettuple != &tuple1 && rettuple != &tuple2) + if (rettuple != NULL && + rettuple != LocTriggerData.tg_trigtuple && + rettuple != LocTriggerData.tg_newtuple) heap_freetuple(rettuple); /* @@ -3577,6 +3765,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events, TriggerDesc *trigdesc = NULL; FmgrInfo *finfo = NULL; Instrumentation *instr = NULL; + TupleTableSlot *slot1 = NULL, + *slot2 = NULL; /* Make a local EState if need be */ if (estate == NULL) @@ -3621,6 +3811,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events, trigdesc = rInfo->ri_TrigDesc; finfo = rInfo->ri_TrigFunctions; instr = rInfo->ri_TrigInstrument; + if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + { + if (slot1 != NULL) + { + ExecDropSingleTupleTableSlot(slot1); + ExecDropSingleTupleTableSlot(slot2); + } + slot1 = MakeSingleTupleTableSlot(rel->rd_att); + slot2 = MakeSingleTupleTableSlot(rel->rd_att); + } if (trigdesc == NULL) /* should not happen */ elog(ERROR, "relation %u has no triggers", evtshared->ats_relid); @@ -3632,7 +3832,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events, * won't try to re-fire it. */ AfterTriggerExecute(event, rel, trigdesc, finfo, instr, - per_tuple_context); + per_tuple_context, slot1, slot2); /* * Mark the event as done. @@ -3663,6 +3863,11 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events, events->tailfree = chunk->freeptr; } } + if (slot1 != NULL) + { + ExecDropSingleTupleTableSlot(slot1); + ExecDropSingleTupleTableSlot(slot2); + } /* Release working resources */ MemoryContextDelete(per_tuple_context); @@ -3712,10 +3917,13 @@ AfterTriggerBeginXact(void) afterTriggers->events.tailfree = NULL; afterTriggers->query_depth = -1; - /* We initialize the query stack to a reasonable size */ + /* We initialize the arrays to a reasonable size */ afterTriggers->query_stack = (AfterTriggerEventList *) MemoryContextAlloc(TopTransactionContext, 8 * sizeof(AfterTriggerEventList)); + afterTriggers->fdw_tuplestores = (Tuplestorestate **) + MemoryContextAllocZero(TopTransactionContext, + 8 * sizeof(Tuplestorestate *)); afterTriggers->maxquerydepth = 8; /* Context for events is created only when needed */ @@ -3756,11 +3964,18 @@ AfterTriggerBeginQuery(void) if (afterTriggers->query_depth >= afterTriggers->maxquerydepth) { /* repalloc will keep the stack in the same context */ - int new_alloc = afterTriggers->maxquerydepth * 2; + int old_alloc = afterTriggers->maxquerydepth; + int new_alloc = old_alloc * 2; afterTriggers->query_stack = (AfterTriggerEventList *) repalloc(afterTriggers->query_stack, new_alloc * sizeof(AfterTriggerEventList)); + afterTriggers->fdw_tuplestores = (Tuplestorestate **) + repalloc(afterTriggers->fdw_tuplestores, + new_alloc * sizeof(Tuplestorestate *)); + /* Clear newly-allocated slots for subsequent lazy initialization. */ + memset(afterTriggers->fdw_tuplestores + old_alloc, + 0, (new_alloc - old_alloc) * sizeof(Tuplestorestate *)); afterTriggers->maxquerydepth = new_alloc; } @@ -3788,6 +4003,7 @@ void AfterTriggerEndQuery(EState *estate) { AfterTriggerEventList *events; + Tuplestorestate *fdw_tuplestore; /* Must be inside a transaction */ Assert(afterTriggers != NULL); @@ -3828,7 +4044,13 @@ AfterTriggerEndQuery(EState *estate) break; } - /* Release query-local storage for events */ + /* Release query-local storage for events, including tuplestore if any */ + fdw_tuplestore = afterTriggers->fdw_tuplestores[afterTriggers->query_depth]; + if (fdw_tuplestore) + { + tuplestore_end(fdw_tuplestore); + afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = NULL; + } afterTriggerFreeEventList(&afterTriggers->query_stack[afterTriggers->query_depth]); afterTriggers->query_depth--; @@ -4050,6 +4272,15 @@ AfterTriggerEndSubXact(bool isCommit) */ while (afterTriggers->query_depth > afterTriggers->depth_stack[my_level]) { + Tuplestorestate *ts; + + ts = afterTriggers->fdw_tuplestores[afterTriggers->query_depth]; + if (ts) + { + tuplestore_end(ts); + afterTriggers->fdw_tuplestores[afterTriggers->query_depth] = NULL; + } + afterTriggerFreeEventList(&afterTriggers->query_stack[afterTriggers->query_depth]); afterTriggers->query_depth--; } @@ -4546,9 +4777,11 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, TriggerDesc *trigdesc = relinfo->ri_TrigDesc; AfterTriggerEventData new_event; AfterTriggerSharedData new_shared; + char relkind = relinfo->ri_RelationDesc->rd_rel->relkind; int tgtype_event; int tgtype_level; int i; + Tuplestorestate *fdw_tuplestore = NULL; /* * Check state. We use normal tests not Asserts because it is possible to @@ -4567,7 +4800,6 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, * validation is important to make sure we don't walk off the edge of our * arrays. */ - new_event.ate_flags = 0; switch (event) { case TRIGGER_EVENT_INSERT: @@ -4612,7 +4844,6 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, Assert(newtup != NULL); ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1)); ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2)); - new_event.ate_flags |= AFTER_TRIGGER_2CTIDS; } else { @@ -4635,6 +4866,11 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, break; } + if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger)) + new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ? + AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID; + /* else, we'll initialize ate_flags for each trigger */ + tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT); for (i = 0; i < trigdesc->numtriggers; i++) @@ -4650,6 +4886,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, modifiedCols, oldtup, newtup)) continue; + if (relkind == RELKIND_FOREIGN_TABLE && row_trigger) + { + if (fdw_tuplestore == NULL) + { + fdw_tuplestore = GetCurrentFDWTuplestore(); + new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH; + } + else + /* subsequent event for the same tuple */ + new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE; + } + /* * If the trigger is a foreign key enforcement trigger, there are * certain cases where we can skip queueing the event because we can @@ -4711,6 +4959,15 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, afterTriggerAddEvent(&afterTriggers->query_stack[afterTriggers->query_depth], &new_event, &new_shared); } + + /* Finally, add the tuple(s) to the tuplestore if needed. */ + if (fdw_tuplestore) + { + if (oldtup != NULL) + tuplestore_puttuple(fdw_tuplestore, oldtup); + if (newtup != NULL) + tuplestore_puttuple(fdw_tuplestore, newtup); + } } Datum diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 6f0f47e..05f1248 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -309,15 +309,17 @@ ExecInsert(TupleTableSlot *slot, * delete and oldtuple is NULL. When deleting from a view, * oldtuple is passed to the INSTEAD OF triggers and identifies * what to delete, and tupleid is invalid. When deleting from a - * foreign table, both tupleid and oldtuple are NULL; the FDW has - * to figure out which row to delete using data from the planSlot. + * foreign table, tupleid is invalid; the FDW has to figure out + * which row to delete using data from the planSlot. oldtuple is + * passed to foreign table triggers; it is NULL when the foreign + * table has no relevant triggers. * * Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */ static TupleTableSlot * ExecDelete(ItemPointer tupleid, - HeapTupleHeader oldtuple, + HeapTuple oldtuple, TupleTableSlot *planSlot, EPQState *epqstate, EState *estate, @@ -342,7 +344,7 @@ ExecDelete(ItemPointer tupleid, bool dodelete; dodelete = ExecBRDeleteTriggers(estate, epqstate, resultRelInfo, - tupleid); + tupleid, oldtuple); if (!dodelete) /* "do nothing" */ return NULL; @@ -352,16 +354,10 @@ ExecDelete(ItemPointer tupleid, if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->trig_delete_instead_row) { - HeapTupleData tuple; bool dodelete; Assert(oldtuple != NULL); - tuple.t_data = oldtuple; - tuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple); - ItemPointerSetInvalid(&(tuple.t_self)); - tuple.t_tableOid = InvalidOid; - - dodelete = ExecIRDeleteTriggers(estate, resultRelInfo, &tuple); + dodelete = ExecIRDeleteTriggers(estate, resultRelInfo, oldtuple); if (!dodelete) /* "do nothing" */ return NULL; @@ -488,7 +484,7 @@ ldelete:; (estate->es_processed)++; /* AFTER ROW DELETE Triggers */ - ExecARDeleteTriggers(estate, resultRelInfo, tupleid); + ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple); /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) @@ -512,10 +508,7 @@ ldelete:; slot = estate->es_trig_tuple_slot; if (oldtuple != NULL) { - deltuple.t_data = oldtuple; - deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple); - ItemPointerSetInvalid(&(deltuple.t_self)); - deltuple.t_tableOid = InvalidOid; + deltuple = *oldtuple; delbuffer = InvalidBuffer; } else @@ -564,15 +557,17 @@ ldelete:; * update and oldtuple is NULL. When updating a view, oldtuple * is passed to the INSTEAD OF triggers and identifies what to * update, and tupleid is invalid. When updating a foreign table, - * both tupleid and oldtuple are NULL; the FDW has to figure out - * which row to update using data from the planSlot. + * tupleid is invalid; the FDW has to figure out which row to + * update using data from the planSlot. oldtuple is passed to + * foreign table triggers; it is NULL when the foreign table has + * no relevant triggers. * * Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */ static TupleTableSlot * ExecUpdate(ItemPointer tupleid, - HeapTupleHeader oldtuple, + HeapTuple oldtuple, TupleTableSlot *slot, TupleTableSlot *planSlot, EPQState *epqstate, @@ -609,7 +604,7 @@ ExecUpdate(ItemPointer tupleid, resultRelInfo->ri_TrigDesc->trig_update_before_row) { slot = ExecBRUpdateTriggers(estate, epqstate, resultRelInfo, - tupleid, slot); + tupleid, oldtuple, slot); if (slot == NULL) /* "do nothing" */ return NULL; @@ -622,16 +617,8 @@ ExecUpdate(ItemPointer tupleid, if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->trig_update_instead_row) { - HeapTupleData oldtup; - - Assert(oldtuple != NULL); - oldtup.t_data = oldtuple; - oldtup.t_len = HeapTupleHeaderGetDatumLength(oldtuple); - ItemPointerSetInvalid(&(oldtup.t_self)); - oldtup.t_tableOid = InvalidOid; - slot = ExecIRUpdateTriggers(estate, resultRelInfo, - &oldtup, slot); + oldtuple, slot); if (slot == NULL) /* "do nothing" */ return NULL; @@ -788,7 +775,7 @@ lreplace:; (estate->es_processed)++; /* AFTER ROW UPDATE Triggers */ - ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, + ExecARUpdateTriggers(estate, resultRelInfo, tupleid, tuple, oldtuple, recheckIndexes); list_free(recheckIndexes); @@ -873,7 +860,8 @@ ExecModifyTable(ModifyTableState *node) TupleTableSlot *planSlot; ItemPointer tupleid = NULL; ItemPointerData tuple_ctid; - HeapTupleHeader oldtuple = NULL; + HeapTupleData oldtupdata; + HeapTuple oldtuple; /* * This should NOT get called during EvalPlanQual; we should have passed a @@ -958,6 +946,7 @@ ExecModifyTable(ModifyTableState *node) EvalPlanQualSetSlot(&node->mt_epqstate, planSlot); slot = planSlot; + oldtuple = NULL; if (junkfilter != NULL) { /* @@ -984,11 +973,15 @@ ExecModifyTable(ModifyTableState *node) * ctid!! */ tupleid = &tuple_ctid; } - else if (relkind == RELKIND_FOREIGN_TABLE) - { - /* do nothing; FDW must fetch any junk attrs it wants */ - } - else + /* + * Foreign table updates have a wholerow attribute when the + * relation has an AFTER ROW trigger. Quite separately, the + * FDW may fetch its own junk attrs to identify the row. + * + * Other relevant relkinds, currently limited to views, always + * have a wholerow attribute. + */ + else if (AttributeNumberIsValid(junkfilter->jf_junkAttNo)) { datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo, @@ -997,8 +990,20 @@ ExecModifyTable(ModifyTableState *node) if (isNull) elog(ERROR, "wholerow is NULL"); - oldtuple = DatumGetHeapTupleHeader(datum); + oldtupdata.t_data = DatumGetHeapTupleHeader(datum); + oldtupdata.t_len = + HeapTupleHeaderGetDatumLength(oldtupdata.t_data); + /* XXX Wrong for foreign tables, but we lack the data. */ + ItemPointerSetInvalid(&(oldtupdata.t_self)); + /* Historically, view triggers see invalid t_tableOid. */ + oldtupdata.t_tableOid = + (relkind == RELKIND_VIEW) ? InvalidOid : + RelationGetRelid(resultRelInfo->ri_RelationDesc); + + oldtuple = &oldtupdata; } + else + Assert(relkind == RELKIND_FOREIGN_TABLE); } /* @@ -1334,7 +1339,11 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) } else if (relkind == RELKIND_FOREIGN_TABLE) { - /* FDW must fetch any junk attrs it wants */ + /* + * When there is an AFTER trigger, there should be a + * wholerow attribute. + */ + j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow"); } else { diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 35bda67..52cc5b7 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -240,7 +240,22 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams) result->commandType = parse->commandType; result->queryId = parse->queryId; - result->hasReturning = (parse->returningList != NIL); + + /* + * Mark the result as having RETURNING only if the returning target list + * has non-resjunk entries + */ + result->hasReturning = false; + foreach(lp, parse->returningList) + { + TargetEntry *tle = lfirst(lp); + + if (!tle->resjunk) + { + result->hasReturning = true; + break; + } + } result->hasModifyingCTE = parse->hasModifyingCTE; result->canSetTag = parse->canSetTag; result->transientPlan = glob->transientPlan; diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 3728d8c..06a2f33 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -51,7 +51,8 @@ static Query *rewriteRuleAction(Query *parsetree, CmdType event, bool *returning_flag); static List *adjustJoinTreeList(Query *parsetree, bool removert, int rt_index); -static void rewriteTargetListIU(Query *parsetree, Relation target_relation, +static void rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte, + Relation target_relation, List **attrno_list); static TargetEntry *process_matched_tle(TargetEntry *src_tle, TargetEntry *prior_tle, @@ -667,6 +668,11 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index) * 4. Sort the tlist into standard order: non-junk fields in order by resno, * then junk fields (these in no particular order). * + * 5. For an INSERT or UPDATE on a foreign table with an AFTER ROW trigger, + * add a whole-row attribute to the RETURNING list. This signals to the FDW + * that it must fetch all attributes from the remote side. XXX Won't this + * confuse code like FetchStatementTargetList()? + * * We must do items 1,2,3 before firing rewrite rules, else rewritten * references to NEW.foo will produce wrong or incomplete results. Item 4 * is not needed for rewriting, but will be needed by the planner, and we @@ -678,8 +684,8 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index) * processing VALUES RTEs. */ static void -rewriteTargetListIU(Query *parsetree, Relation target_relation, - List **attrno_list) +rewriteTargetListIU(Query *parsetree, RangeTblEntry *target_rte, + Relation target_relation, List **attrno_list) { CmdType commandType = parsetree->commandType; TargetEntry **new_tles; @@ -755,6 +761,30 @@ rewriteTargetListIU(Query *parsetree, Relation target_relation, } } + /* + * For foreign tables, force RETURNING the whole-row if a corresponding + * AFTER trigger is found + */ + if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE && + target_relation->trigdesc && + ((commandType == CMD_INSERT && + target_relation->trigdesc->trig_insert_after_row) || + (commandType == CMD_UPDATE && + target_relation->trigdesc->trig_update_after_row))) + + { + Var *var = makeWholeRowVar(target_rte, + parsetree->resultRelation, + 0, + false); + TargetEntry *tle = makeTargetEntry((Expr *) var, + list_length(parsetree->returningList) + 1, + "wholerow", + true); + + parsetree->returningList = lappend(parsetree->returningList, tle); + } + for (attrno = 1; attrno <= numattrs; attrno++) { TargetEntry *new_tle = new_tles[attrno - 1]; @@ -1199,7 +1229,7 @@ static void rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte, Relation target_relation) { - Var *var; + Var *var = NULL; const char *attrname; TargetEntry *tle; @@ -1231,7 +1261,26 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte, fdwroutine->AddForeignUpdateTargets(parsetree, target_rte, target_relation); - return; + /* + * If we have a trigger corresponding to the operation, add a wholerow + * attribute. XXX This misses system columns. + */ + if (target_relation->trigdesc && + ((parsetree->commandType == CMD_UPDATE && + (target_relation->trigdesc->trig_update_after_row + || target_relation->trigdesc->trig_update_before_row)) || + (parsetree->commandType == CMD_DELETE && + (target_relation->trigdesc->trig_delete_after_row || + target_relation->trigdesc->trig_delete_before_row)))) + { + var = makeWholeRowVar(target_rte, + parsetree->resultRelation, + 0, + false); + + attrname = "wholerow"; + + } } else { @@ -1247,12 +1296,15 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte, attrname = "wholerow"; } - tle = makeTargetEntry((Expr *) var, - list_length(parsetree->targetList) + 1, - pstrdup(attrname), - true); + if (var != NULL) + { + tle = makeTargetEntry((Expr *) var, + list_length(parsetree->targetList) + 1, + pstrdup(attrname), + true); - parsetree->targetList = lappend(parsetree->targetList, tle); + parsetree->targetList = lappend(parsetree->targetList, tle); + } } @@ -2993,19 +3045,19 @@ RewriteQuery(Query *parsetree, List *rewrite_events) List *attrnos; /* Process the main targetlist ... */ - rewriteTargetListIU(parsetree, rt_entry_relation, &attrnos); + rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, &attrnos); /* ... and the VALUES expression lists */ rewriteValuesRTE(values_rte, rt_entry_relation, attrnos); } else { /* Process just the main targetlist */ - rewriteTargetListIU(parsetree, rt_entry_relation, NULL); + rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL); } } else if (event == CMD_UPDATE) { - rewriteTargetListIU(parsetree, rt_entry_relation, NULL); + rewriteTargetListIU(parsetree, rt_entry, rt_entry_relation, NULL); rewriteTargetListUD(parsetree, rt_entry, rt_entry_relation); } else if (event == CMD_DELETE) diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index 18cb128..84bc984 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -147,10 +147,12 @@ extern void ExecASDeleteTriggers(EState *estate, extern bool ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, - ItemPointer tupleid); + ItemPointer tupleid, + HeapTuple fdw_trigtuple); extern void ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, - ItemPointer tupleid); + ItemPointer tupleid, + HeapTuple fdw_trigtuple); extern bool ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, HeapTuple trigtuple); @@ -162,11 +164,13 @@ extern TupleTableSlot *ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, ItemPointer tupleid, + HeapTuple fdw_trigtuple, TupleTableSlot *slot); extern void ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, HeapTuple newtuple, + HeapTuple fdw_trigtuple, List *recheckIndexes); extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index 60506e0..c34c9b4 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -1158,6 +1158,43 @@ CREATE USER MAPPING FOR current_user SERVER s9; DROP SERVER s9 CASCADE; -- ERROR ERROR: must be owner of foreign server s9 RESET ROLE; +-- Triggers +CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$ + BEGIN + RETURN NULL; + END +$$ language plpgsql; +CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH STATEMENT +EXECUTE PROCEDURE dummy_trigger(); +CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH STATEMENT +EXECUTE PROCEDURE dummy_trigger(); +CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH ROW +EXECUTE PROCEDURE dummy_trigger(); +CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH ROW +EXECUTE PROCEDURE dummy_trigger(); +CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH ROW +EXECUTE PROCEDURE dummy_trigger(); +ERROR: "foreign_table_1" is a foreign table +DETAIL: Foreign tables cannot have constraint triggers. +ALTER FOREIGN TABLE foreign_schema.foreign_table_1 + DISABLE TRIGGER trigtest_before_stmt; +ALTER FOREIGN TABLE foreign_schema.foreign_table_1 + ENABLE TRIGGER trigtest_before_stmt; +DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1; +DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1; +DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1; +DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1; +DROP FUNCTION dummy_trigger(); -- DROP FOREIGN TABLE DROP FOREIGN TABLE no_table; -- ERROR ERROR: foreign table "no_table" does not exist diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql index f819eb1..0f0869e 100644 --- a/src/test/regress/sql/foreign_data.sql +++ b/src/test/regress/sql/foreign_data.sql @@ -470,6 +470,50 @@ CREATE USER MAPPING FOR current_user SERVER s9; DROP SERVER s9 CASCADE; -- ERROR RESET ROLE; +-- Triggers +CREATE FUNCTION dummy_trigger() RETURNS TRIGGER AS $$ + BEGIN + RETURN NULL; + END +$$ language plpgsql; + +CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH STATEMENT +EXECUTE PROCEDURE dummy_trigger(); + +CREATE TRIGGER trigtest_after_stmt AFTER INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH STATEMENT +EXECUTE PROCEDURE dummy_trigger(); + +CREATE TRIGGER trigtest_before_row BEFORE INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH ROW +EXECUTE PROCEDURE dummy_trigger(); + +CREATE TRIGGER trigtest_after_row AFTER INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH ROW +EXECUTE PROCEDURE dummy_trigger(); + +CREATE CONSTRAINT TRIGGER trigtest_constraint AFTER INSERT OR UPDATE OR DELETE +ON foreign_schema.foreign_table_1 +FOR EACH ROW +EXECUTE PROCEDURE dummy_trigger(); + +ALTER FOREIGN TABLE foreign_schema.foreign_table_1 + DISABLE TRIGGER trigtest_before_stmt; +ALTER FOREIGN TABLE foreign_schema.foreign_table_1 + ENABLE TRIGGER trigtest_before_stmt; + +DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1; +DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1; +DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1; +DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1; + +DROP FUNCTION dummy_trigger(); + -- DROP FOREIGN TABLE DROP FOREIGN TABLE no_table; -- ERROR DROP FOREIGN TABLE IF EXISTS no_table;