]> granicus.if.org Git - postgresql/commitdiff
Offer triggers on foreign tables.
authorNoah Misch <noah@leadboat.com>
Sun, 23 Mar 2014 06:16:34 +0000 (02:16 -0400)
committerNoah Misch <noah@leadboat.com>
Sun, 23 Mar 2014 06:16:34 +0000 (02:16 -0400)
This covers all the SQL-standard trigger types supported for regular
tables; it does not cover constraint triggers.  The approach for
acquiring the old row mirrors that for view INSTEAD OF triggers.  For
AFTER ROW triggers, we spool the foreign tuples to a tuplestore.

This changes the FDW API contract; when deciding which columns to
populate in the slot returned from data modification callbacks, writable
FDWs will need to check for AFTER ROW triggers in addition to checking
for a RETURNING clause.

In support of the feature addition, refactor the TriggerFlags bits and
the assembly of old tuples in ModifyTable.

Ronan Dunklau, reviewed by KaiGai Kohei; some additional hacking by me.

14 files changed:
contrib/postgres_fdw/deparse.c
contrib/postgres_fdw/expected/postgres_fdw.out
contrib/postgres_fdw/postgres_fdw.c
contrib/postgres_fdw/sql/postgres_fdw.sql
doc/src/sgml/fdwhandler.sgml
doc/src/sgml/ref/create_trigger.sgml
doc/src/sgml/trigger.sgml
src/backend/commands/tablecmds.c
src/backend/commands/trigger.c
src/backend/executor/nodeModifyTable.c
src/backend/rewrite/rewriteHandler.c
src/include/commands/trigger.h
src/test/regress/expected/foreign_data.out
src/test/regress/sql/foreign_data.sql

index 2dfe80da0afe0301b7ba9648ad5faf8ded242b7f..32c0135071468d3b6a818886267d73eb6ed38cc5 100644 (file)
@@ -110,6 +110,7 @@ static void deparseTargetList(StringInfo buf,
                                  List **retrieved_attrs);
 static void deparseReturningList(StringInfo buf, PlannerInfo *root,
                                         Index rtindex, Relation rel,
+                                        bool trig_after_row,
                                         List *returningList,
                                         List **retrieved_attrs);
 static void deparseColumnRef(StringInfo buf, int varno, int varattno,
@@ -875,11 +876,9 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
        else
                appendStringInfoString(buf, " DEFAULT VALUES");
 
-       if (returningList)
-               deparseReturningList(buf, root, rtindex, rel, returningList,
-                                                        retrieved_attrs);
-       else
-               *retrieved_attrs = NIL;
+       deparseReturningList(buf, root, rtindex, rel,
+                                          rel->trigdesc && rel->trigdesc->trig_insert_after_row,
+                                                returningList, retrieved_attrs);
 }
 
 /*
@@ -919,11 +918,9 @@ deparseUpdateSql(StringInfo buf, PlannerInfo *root,
        }
        appendStringInfoString(buf, " WHERE ctid = $1");
 
-       if (returningList)
-               deparseReturningList(buf, root, rtindex, rel, returningList,
-                                                        retrieved_attrs);
-       else
-               *retrieved_attrs = NIL;
+       deparseReturningList(buf, root, rtindex, rel,
+                                          rel->trigdesc && rel->trigdesc->trig_update_after_row,
+                                                returningList, retrieved_attrs);
 }
 
 /*
@@ -943,34 +940,48 @@ deparseDeleteSql(StringInfo buf, PlannerInfo *root,
        deparseRelation(buf, rel);
        appendStringInfoString(buf, " WHERE ctid = $1");
 
-       if (returningList)
-               deparseReturningList(buf, root, rtindex, rel, returningList,
-                                                        retrieved_attrs);
-       else
-               *retrieved_attrs = NIL;
+       deparseReturningList(buf, root, rtindex, rel,
+                                          rel->trigdesc && rel->trigdesc->trig_delete_after_row,
+                                                returningList, retrieved_attrs);
 }
 
 /*
- * deparse RETURNING clause of INSERT/UPDATE/DELETE
+ * Add a RETURNING clause, if needed, to an INSERT/UPDATE/DELETE.
  */
 static void
 deparseReturningList(StringInfo buf, PlannerInfo *root,
                                         Index rtindex, Relation rel,
+                                        bool trig_after_row,
                                         List *returningList,
                                         List **retrieved_attrs)
 {
-       Bitmapset  *attrs_used;
+       Bitmapset  *attrs_used = NULL;
 
-       /*
-        * We need the attrs mentioned in the query's RETURNING list.
-        */
-       attrs_used = NULL;
-       pull_varattnos((Node *) returningList, rtindex,
-                                  &attrs_used);
+       if (trig_after_row)
+       {
+               /* whole-row reference acquires all non-system columns */
+               attrs_used =
+                       bms_make_singleton(0 - FirstLowInvalidHeapAttributeNumber);
+       }
 
-       appendStringInfoString(buf, " RETURNING ");
-       deparseTargetList(buf, root, rtindex, rel, attrs_used,
-                                         retrieved_attrs);
+       if (returningList != NIL)
+       {
+               /*
+                * We need the attrs, non-system and system, mentioned in the local
+                * query's RETURNING list.
+                */
+               pull_varattnos((Node *) returningList, rtindex,
+                                          &attrs_used);
+       }
+
+       if (attrs_used != NULL)
+       {
+               appendStringInfoString(buf, " RETURNING ");
+               deparseTargetList(buf, root, rtindex, rel, attrs_used,
+                                                 retrieved_attrs);
+       }
+       else
+               *retrieved_attrs = NIL;
 }
 
 /*
index 9a3d651667286c4eabcb37826176285a80d12b79..671c329c9204fe191a35b1179756e0b0e241f147 100644 (file)
@@ -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(<NULL>) 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(<NULL>) called: action = DELETE, when = AFTER, level = STATEMENT
+insert into rem1 values(1,'insert');
+NOTICE:  trigger_func(<NULL>) 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(<NULL>) called: action = INSERT, when = AFTER, level = STATEMENT
+update rem1 set f2  = 'update' where f1 = 1;
+NOTICE:  trigger_func(<NULL>) 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(<NULL>) called: action = UPDATE, when = AFTER, level = STATEMENT
+update rem1 set f2 = f2 || f2;
+NOTICE:  trigger_func(<NULL>) 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(<NULL>) 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 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)
+
index 1ae1c184372b4e4508ccd5873d918329c544810b..d7c5fa21195a86e9f24ba0937e63a193cb249933 100644 (file)
@@ -108,7 +108,7 @@ enum FdwScanPrivateIndex
  * 1) INSERT/UPDATE/DELETE statement text to be sent to the remote server
  * 2) Integer list of target attribute numbers for INSERT/UPDATE
  *       (NIL for a DELETE)
- * 3) Boolean flag showing if there's a RETURNING clause
+ * 3) Boolean flag showing if the remote query has a RETURNING clause
  * 4) Integer list of attribute numbers retrieved by RETURNING, if any
  */
 enum FdwModifyPrivateIndex
@@ -1246,7 +1246,7 @@ postgresPlanForeignModify(PlannerInfo *root,
         */
        return list_make4(makeString(sql.data),
                                          targetAttrs,
-                                         makeInteger((returningList != NIL)),
+                                         makeInteger((retrieved_attrs != NIL)),
                                          retrieved_attrs);
 }
 
index 21b15ca9ff208422fb248b6ca054fccb725b3429..d47ceca3c11d83a0223b467ff56dc4c7846128ca 100644 (file)
@@ -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 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;
index 6c06f1a436793ad822c1bbf87e116aad9da405b6..9c818cd594364e591057fac2d83ef1c20e8b4962 100644 (file)
@@ -308,7 +308,8 @@ AddForeignUpdateTargets (Query *parsetree,
      extra values to be fetched.  Each such entry must be marked
      <structfield>resjunk</> = <literal>true</>, and must have a distinct
      <structfield>resname</> that will identify it at execution time.
-     Avoid using names matching <literal>ctid<replaceable>N</></literal> or
+     Avoid using names matching <literal>ctid<replaceable>N</></literal>,
+     <literal>wholerow</literal>, or
      <literal>wholerow<replaceable>N</></literal>, as the core system can
      generate junk columns of these names.
     </para>
@@ -447,11 +448,12 @@ ExecForeignInsert (EState *estate,
 
     <para>
      The data in the returned slot is used only if the <command>INSERT</>
-     query has a <literal>RETURNING</> clause.  Hence, the FDW could choose
-     to optimize away returning some or all columns depending on the contents
-     of the <literal>RETURNING</> clause.  However, some slot must be
-     returned to indicate success, or the query's reported row count will be
-     wrong.
+     query has a <literal>RETURNING</> clause or the foreign table has
+     an <literal>AFTER ROW</> trigger.  Triggers require all columns, but the
+     FDW could choose to optimize away returning some or all columns depending
+     on the contents of the <literal>RETURNING</> clause.  Regardless, some
+     slot must be returned to indicate success, or the query's reported row
+     count will be wrong.
     </para>
 
     <para>
@@ -492,11 +494,12 @@ ExecForeignUpdate (EState *estate,
 
     <para>
      The data in the returned slot is used only if the <command>UPDATE</>
-     query has a <literal>RETURNING</> clause.  Hence, the FDW could choose
-     to optimize away returning some or all columns depending on the contents
-     of the <literal>RETURNING</> clause.  However, some slot must be
-     returned to indicate success, or the query's reported row count will be
-     wrong.
+     query has a <literal>RETURNING</> clause or the foreign table has
+     an <literal>AFTER ROW</> trigger.  Triggers require all columns, but the
+     FDW could choose to optimize away returning some or all columns depending
+     on the contents of the <literal>RETURNING</> clause.  Regardless, some
+     slot must be returned to indicate success, or the query's reported row
+     count will be wrong.
     </para>
 
     <para>
@@ -535,11 +538,12 @@ ExecForeignDelete (EState *estate,
 
     <para>
      The data in the returned slot is used only if the <command>DELETE</>
-     query has a <literal>RETURNING</> clause.  Hence, the FDW could choose
-     to optimize away returning some or all columns depending on the contents
-     of the <literal>RETURNING</> clause.  However, some slot must be
-     returned to indicate success, or the query's reported row count will be
-     wrong.
+     query has a <literal>RETURNING</> clause or the foreign table has
+     an <literal>AFTER ROW</> trigger.  Triggers require all columns, but the
+     FDW could choose to optimize away returning some or all columns depending
+     on the contents of the <literal>RETURNING</> clause.  Regardless, some
+     slot must be returned to indicate success, or the query's reported row
+     count will be wrong.
     </para>
 
     <para>
index a8fba49e4c3ca3bb901d89b5515538b0ea764f50..d270d66c574f259eda65826c65efcc0f4fcb20fc 100644 (file)
@@ -43,9 +43,10 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
 
   <para>
    <command>CREATE TRIGGER</command> creates a new trigger.  The
-   trigger will be associated with the specified table or view and will
-   execute the specified function <replaceable
-   class="parameter">function_name</replaceable> when certain events occur.
+   trigger will be associated with the specified table, view, or foreign table
+   and will execute the specified
+   function <replaceable class="parameter">function_name</replaceable> when
+   certain events occur.
   </para>
 
   <para>
@@ -93,7 +94,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
 
   <para>
    The following table summarizes which types of triggers may be used on
-   tables and views:
+   tables, views, and foreign tables:
   </para>
 
   <informaltable id="supported-trigger-types">
@@ -110,8 +111,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
      <row>
       <entry align="center" morerows="1"><literal>BEFORE</></entry>
       <entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
-      <entry align="center">Tables</entry>
-      <entry align="center">Tables and views</entry>
+      <entry align="center">Tables and foreign tables</entry>
+      <entry align="center">Tables, views, and foreign tables</entry>
      </row>
      <row>
       <entry align="center"><command>TRUNCATE</></entry>
@@ -121,8 +122,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
      <row>
       <entry align="center" morerows="1"><literal>AFTER</></entry>
       <entry align="center"><command>INSERT</>/<command>UPDATE</>/<command>DELETE</></entry>
-      <entry align="center">Tables</entry>
-      <entry align="center">Tables and views</entry>
+      <entry align="center">Tables and foreign tables</entry>
+      <entry align="center">Tables, views, and foreign tables</entry>
      </row>
      <row>
       <entry align="center"><command>TRUNCATE</></entry>
@@ -164,13 +165,13 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
    <firstterm>constraint trigger</>.  This is the same as a regular trigger
    except that the timing of the trigger firing can be adjusted using
    <xref linkend="SQL-SET-CONSTRAINTS">.
-   Constraint triggers must be <literal>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 <firstterm>deferred</>.  A pending deferred-trigger firing can
-   also be forced to happen immediately by using <command>SET CONSTRAINTS</>.
-   Constraint triggers are expected to raise an exception when the constraints
-   they implement are violated.
+   Constraint triggers must be <literal>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 <firstterm>deferred</>.  A pending deferred-trigger firing
+   can also be forced to happen immediately by using <command>SET
+   CONSTRAINTS</>.  Constraint triggers are expected to raise an exception
+   when the constraints they implement are violated.
   </para>
 
   <para>
@@ -244,8 +245,8 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
     <term><replaceable class="parameter">table_name</replaceable></term>
     <listitem>
      <para>
-      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.
      </para>
     </listitem>
    </varlistentry>
@@ -481,6 +482,14 @@ CREATE TRIGGER view_insert
  <refsect1 id="SQL-CREATETRIGGER-compatibility">
   <title>Compatibility</title>
 
+  <!--
+   It's not clear whether SQL/MED contemplates triggers on foreign tables.
+   Its <drop basic column definition> General Rules do mention the possibility
+   of a reference from a trigger column list.  On the other hand, nothing
+   overrides the fact that CREATE TRIGGER only targets base tables.  For now,
+   do not document the compatibility status of triggers on foreign tables.
+  -->
+
   <para>
    The <command>CREATE TRIGGER</command> statement in
    <productname>PostgreSQL</productname> implements a subset of the
index f579340e48f1e66aa0b5c2723b820914ee0e7ba9..f94aea174abbbeda8386362cbde04b13b9a173ee 100644 (file)
    <para>
     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.
   </para>
 
   <para>
-    On tables, triggers can be defined to execute either before or after any
-    <command>INSERT</command>, <command>UPDATE</command>, or
-    <command>DELETE</command> operation, either once per modified row,
+    On tables and foreign tables, triggers can be defined to execute either
+    before or after any <command>INSERT</command>, <command>UPDATE</command>,
+    or <command>DELETE</command> operation, either once per modified row,
     or once per <acronym>SQL</acronym> statement.
     <command>UPDATE</command> triggers can moreover be set to fire only if
     certain columns are mentioned in the <literal>SET</literal> clause of the
     <command>UPDATE</command> statement.
     Triggers can also fire for <command>TRUNCATE</command> 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.
    </para>
 
    <para>
     triggers fire immediately before a particular row is operated on,
     while row-level <literal>AFTER</> triggers fire at the end of the
     statement (but before any statement-level <literal>AFTER</> triggers).
-    These types of triggers may only be defined on tables.  Row-level
-    <literal>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 <literal>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.
    </para>
 
    <para>
@@ -548,7 +549,8 @@ typedef struct TriggerData
         <command>DELETE</command> then this is what you should return
         from the function if you don't want to replace the row with
         a different one (in the case of <command>INSERT</command>) or
-        skip the operation.
+        skip the operation.  For triggers on foreign tables, values of system
+        columns herein are unspecified.
        </para>
       </listitem>
      </varlistentry>
@@ -563,7 +565,8 @@ typedef struct TriggerData
         <command>DELETE</command>. This is what you have to return
         from the function if the event is an <command>UPDATE</command>
         and you don't want to replace this row by a different one or
-        skip the operation.
+        skip the operation.  For triggers on foreign tables, values of system
+        columns herein are unspecified.
        </para>
       </listitem>
      </varlistentry>
index 25f01e5165f2d8b6216cca0fdc9e22398f95a9ab..7f3f730a87ba7b17def7467e4b7f9e3808712d29 100644 (file)
@@ -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:
index 3e92a7c29e519cdf04abd5c6fc424ba924e88bde..5f1ccf02c27f818b2f5e38fd8cabff1e992e4f09 100644 (file)
@@ -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)
        {
@@ -2464,24 +2521,33 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
 
 void
 ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
-                                        ItemPointer tupleid, HeapTuple newtuple,
+                                        ItemPointer tupleid,
+                                        HeapTuple fdw_trigtuple,
+                                        HeapTuple newtuple,
                                         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 +3008,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 +3038,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 +3066,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 +3157,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 +3190,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 +3207,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 +3501,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 +3548,76 @@ 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;
+
+                       LocTriggerData.tg_newtuple =
+                               ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
+                                TRIGGER_EVENT_UPDATE) ?
+                               ExecMaterializeSlot(trig_tuple_slot2) : 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 +3639,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 +3761,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 +3807,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 +3828,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 +3859,11 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
                                events->tailfree = chunk->freeptr;
                }
        }
+       if (slot1 != NULL)
+       {
+               ExecDropSingleTupleTableSlot(slot1);
+               ExecDropSingleTupleTableSlot(slot2);
+       }
 
        /* Release working resources */
        MemoryContextDelete(per_tuple_context);
@@ -3712,10 +3913,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 +3960,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 +3999,7 @@ void
 AfterTriggerEndQuery(EState *estate)
 {
        AfterTriggerEventList *events;
+       Tuplestorestate *fdw_tuplestore;
 
        /* Must be inside a transaction */
        Assert(afterTriggers != NULL);
@@ -3832,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--;
@@ -4056,6 +4274,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--;
                }
@@ -4552,9 +4779,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
@@ -4573,7 +4802,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:
@@ -4618,7 +4846,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
                        {
@@ -4641,6 +4868,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++)
@@ -4656,6 +4888,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
@@ -4717,6 +4961,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
                afterTriggerAddEvent(&afterTriggers->query_stack[afterTriggers->query_depth],
                                                         &new_event, &new_shared);
        }
+
+       /*
+        * Finally, spool any foreign tuple(s).  The tuplestore squashes them to
+        * minimal tuples, so this loses any system columns.  The executor lost
+        * those columns before us, for an unrelated reason, so this is fine.
+        */
+       if (fdw_tuplestore)
+       {
+               if (oldtup != NULL)
+                       tuplestore_puttuple(fdw_tuplestore, oldtup);
+               if (newtup != NULL)
+                       tuplestore_puttuple(fdw_tuplestore, newtup);
+       }
 }
 
 Datum
index 6f0f47e7ce33da6aab3bc6e1472389f052d30022..fca7a2581f38b98a43f0400a8a5729354f533e40 100644 (file)
@@ -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, oldtuple, tuple,
                                                 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,21 @@ ExecModifyTable(ModifyTableState *node)
                                                                                                 * ctid!! */
                                        tupleid = &tuple_ctid;
                                }
-                               else if (relkind == RELKIND_FOREIGN_TABLE)
-                               {
-                                       /* do nothing; FDW must fetch any junk attrs it wants */
-                               }
-                               else
+                               /*
+                                * Use the wholerow attribute, when available, to reconstruct
+                                * the old relation tuple.
+                                *
+                                * Foreign table updates have a wholerow attribute when the
+                                * relation has an AFTER ROW trigger.  Note that the wholerow
+                                * attribute does not carry system columns.  Foreign table
+                                * triggers miss seeing those, except that we know enough here
+                                * to set t_tableOid.  Quite separately from this, 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 +996,19 @@ 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);
+                                       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 +1344,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
                                        {
index 3728d8c418c7e1bbf9298155df58231de7016d3d..5dbcce3e550b0ed5a76e86361fc4442c933e771c 100644 (file)
@@ -1199,7 +1199,7 @@ static void
 rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
                                        Relation target_relation)
 {
-       Var                *var;
+       Var                *var = NULL;
        const char *attrname;
        TargetEntry *tle;
 
@@ -1231,7 +1231,26 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
                        fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
                                                                                                target_relation);
 
-               return;
+               /*
+                * If we have a row-level trigger corresponding to the operation, emit
+                * a whole-row Var so that executor will have the "old" row to pass to
+                * the trigger.  Alas, 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 +1266,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);
+       }
 }
 
 
index 18cb128ed4db8103e58f12631f483825a3f7d46e..d0b0356ba6de980bfec562efb3b4631ab56f4af1 100644 (file)
@@ -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,10 +164,12 @@ 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 fdw_trigtuple,
                                         HeapTuple newtuple,
                                         List *recheckIndexes);
 extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
index 60506e07b1c94fcfb6e9ab8adb6f016b4a84b7f2..c34c9b4df42fcd1029e9e71088b69fd23e879e91 100644 (file)
@@ -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
index f819eb1b8eb3c9eed49533fd1c287b57fe8632d1..0f0869ee268c3afb54defec8818ecb8941fabd34 100644 (file)
@@ -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;