Offer triggers on foreign tables.
authorNoah Misch
Sun, 23 Mar 2014 06:16:34 +0000 (02:16 -0400)
committerNoah Misch
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() called: action = DELETE, when = BEFORE, level = STATEMENT
+NOTICE:  trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE:  OLD: (1,hi)
+NOTICE:  trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE:  OLD: (10,"hi remote")
+NOTICE:  trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE:  OLD: (2,bye)
+NOTICE:  trig_row_before(23, skidoo) BEFORE ROW DELETE ON rem1
+NOTICE:  OLD: (11,"bye remote")
+NOTICE:  trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE:  OLD: (1,hi)
+NOTICE:  trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE:  OLD: (10,"hi remote")
+NOTICE:  trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE:  OLD: (2,bye)
+NOTICE:  trig_row_after(23, skidoo) AFTER ROW DELETE ON rem1
+NOTICE:  OLD: (11,"bye remote")
+NOTICE:  trigger_func() called: action = DELETE, when = AFTER, level = STATEMENT
+insert into rem1 values(1,'insert');
+NOTICE:  trigger_func() called: action = INSERT, when = BEFORE, level = STATEMENT
+NOTICE:  trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem1
+NOTICE:  NEW: (1,insert)
+NOTICE:  trig_row_after(23, skidoo) AFTER ROW INSERT ON rem1
+NOTICE:  NEW: (1,insert)
+NOTICE:  trigger_func() called: action = INSERT, when = AFTER, level = STATEMENT
+update rem1 set f2  = 'update' where f1 = 1;
+NOTICE:  trigger_func() called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE:  trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE:  OLD: (1,insert),NEW: (1,update)
+NOTICE:  trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE:  OLD: (1,insert),NEW: (1,update)
+NOTICE:  trigger_func() called: action = UPDATE, when = AFTER, level = STATEMENT
+update rem1 set f2 = f2 || f2;
+NOTICE:  trigger_func() called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE:  trig_row_before(23, skidoo) BEFORE ROW UPDATE ON rem1
+NOTICE:  OLD: (1,update),NEW: (1,updateupdate)
+NOTICE:  trig_row_after(23, skidoo) AFTER ROW UPDATE ON rem1
+NOTICE:  OLD: (1,update),NEW: (1,updateupdate)
+NOTICE:  trigger_func() called: action = UPDATE, when = AFTER, level = STATEMENT
+-- cleanup
+DROP TRIGGER trig_row_before ON rem1;
+DROP TRIGGER trig_row_after ON rem1;
+DROP TRIGGER trig_stmt_before ON rem1;
+DROP TRIGGER trig_stmt_after ON rem1;
+DELETE from rem1;
+-- Test WHEN conditions
+CREATE TRIGGER trig_row_before_insupd
+BEFORE INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER trig_row_after_insupd
+AFTER INSERT OR UPDATE ON rem1
+FOR EACH ROW
+WHEN (NEW.f2 like '%update%')
+EXECUTE PROCEDURE trigger_data(23,'skidoo');
+-- Insert or update not matching: nothing happens
+INSERT INTO rem1 values(1, 'insert');
+UPDATE rem1 set f2 = 'test';
+-- Insert or update 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
      resjunk = true, and must have a distinct
      resname that will identify it at execution time.
-     Avoid using names matching ctidN or
+     Avoid using names matching ctidN,
+     wholerow, or
      wholerowN, as the core system can
      generate junk columns of these names.
     
@@ -447,11 +448,12 @@ ExecForeignInsert (EState *estate,
 
     
      The data in the returned slot is used only if the INSERT
-     query has a RETURNING clause.  Hence, the FDW could choose
-     to optimize away returning some or all columns depending on the contents
-     of the RETURNING clause.  However, some slot must be
-     returned to indicate success, or the query's reported row count will be
-     wrong.
+     query has a RETURNING clause or the foreign table has
+     an 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 RETURNING clause.  Regardless, some
+     slot must be returned to indicate success, or the query's reported row
+     count will be wrong.
     
 
     
@@ -492,11 +494,12 @@ ExecForeignUpdate (EState *estate,
 
     
      The data in the returned slot is used only if the UPDATE
-     query has a RETURNING clause.  Hence, the FDW could choose
-     to optimize away returning some or all columns depending on the contents
-     of the RETURNING clause.  However, some slot must be
-     returned to indicate success, or the query's reported row count will be
-     wrong.
+     query has a RETURNING clause or the foreign table has
+     an 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 RETURNING clause.  Regardless, some
+     slot must be returned to indicate success, or the query's reported row
+     count will be wrong.
     
 
     
@@ -535,11 +538,12 @@ ExecForeignDelete (EState *estate,
 
     
      The data in the returned slot is used only if the DELETE
-     query has a RETURNING clause.  Hence, the FDW could choose
-     to optimize away returning some or all columns depending on the contents
-     of the RETURNING clause.  However, some slot must be
-     returned to indicate success, or the query's reported row count will be
-     wrong.
+     query has a RETURNING clause or the foreign table has
+     an 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 RETURNING clause.  Regardless, some
+     slot must be returned to indicate success, or the query's reported row
+     count will be wrong.
     
 
     
index a8fba49e4c3ca3bb901d89b5515538b0ea764f50..d270d66c574f259eda65826c65efcc0f4fcb20fc 100644 (file)
@@ -43,9 +43,10 @@ CREATE [ CONSTRAINT ] TRIGGER name
 
   
    CREATE TRIGGER creates a new trigger.  The
-   trigger will be associated with the specified table or view and will
-   execute the specified function 
-   class="parameter">function_name when certain events occur.
+   trigger will be associated with the specified table, view, or foreign table
+   and will execute the specified
+   function function_name when
+   certain events occur.
   
 
   
@@ -93,7 +94,7 @@ CREATE [ CONSTRAINT ] TRIGGER name
 
   
    The following table summarizes which types of triggers may be used on
-   tables and views:
+   tables, views, and foreign tables:
   
 
   
@@ -110,8 +111,8 @@ CREATE [ CONSTRAINT ] TRIGGER name
      
       BEFORE
       INSERT/UPDATE/DELETE
-      Tables
-      Tables and views
+      Tables and foreign tables
+      Tables, views, and foreign tables
      
      
       TRUNCATE
@@ -121,8 +122,8 @@ CREATE [ CONSTRAINT ] TRIGGER name
      
       AFTER
       INSERT/UPDATE/DELETE
-      Tables
-      Tables and views
+      Tables and foreign tables
+      Tables, views, and foreign tables
      
      
       TRUNCATE
@@ -164,13 +165,13 @@ CREATE [ CONSTRAINT ] TRIGGER name
    constraint trigger.  This is the same as a regular trigger
    except that the timing of the trigger firing can be adjusted using
    .
-   Constraint triggers must be AFTER ROW triggers.  They can
-   be fired either at the end of the statement causing the triggering event,
-   or at the end of the containing transaction; in the latter case they are
-   said to be deferred.  A pending deferred-trigger firing can
-   also be forced to happen immediately by using SET CONSTRAINTS.
-   Constraint triggers are expected to raise an exception when the constraints
-   they implement are violated.
+   Constraint triggers must be AFTER ROW triggers on tables.  They
+   can be fired either at the end of the statement causing the triggering
+   event, or at the end of the containing transaction; in the latter case they
+   are said to be deferred.  A pending deferred-trigger firing
+   can also be forced to happen immediately by using SET
+   CONSTRAINTS.  Constraint triggers are expected to raise an exception
+   when the constraints they implement are violated.
   
 
   
@@ -244,8 +245,8 @@ UPDATE OF column_name1 [, column_name2
     table_name
     
      
-      The name (optionally schema-qualified) of the table or view the trigger
-      is for.
+      The name (optionally schema-qualified) of the table, view, or foreign
+      table the trigger is for.
      
     
    
@@ -481,6 +482,14 @@ CREATE TRIGGER view_insert
  
   Compatibility
 
+  
+
   
    The CREATE TRIGGER statement in
    PostgreSQL implements a subset of the
index f579340e48f1e66aa0b5c2723b820914ee0e7ba9..f94aea174abbbeda8386362cbde04b13b9a173ee 100644 (file)
    
     A trigger is a specification that the database should automatically
     execute a particular function whenever a certain type of operation is
-    performed.  Triggers can be attached to both tables and views.
+    performed.  Triggers can be attached to tables, views, and foreign tables.
   
 
   
-    On tables, triggers can be defined to execute either before or after any
-    INSERTUPDATE, or
-    DELETE operation, either once per modified row,
+    On tables and foreign tables, triggers can be defined to execute either
+    before or after any INSERTUPDATE,
+    or DELETE operation, either once per modified row,
     or once per SQL statement.
     UPDATE triggers can moreover be set to fire only if
     certain columns are mentioned in the SET clause of the
     UPDATE statement.
     Triggers can also fire for TRUNCATE statements.
     If a trigger event occurs, the trigger's function is called at the
-    appropriate time to handle the event.
+    appropriate time to handle the event.  Foreign tables do not support the
+    TRUNCATE statement at all.
    
 
    
     triggers fire immediately before a particular row is operated on,
     while row-level AFTER triggers fire at the end of the
     statement (but before any statement-level AFTER triggers).
-    These types of triggers may only be defined on tables.  Row-level
-    INSTEAD OF triggers may only be defined on views, and fire
-    immediately as each row in the view is identified as needing to be
-    operated on.
+    These types of triggers may only be defined on tables and foreign tables.
+    Row-level INSTEAD OF triggers may only be defined on views,
+    and fire immediately as each row in the view is identified as needing to
+    be operated on.
    
 
    
@@ -548,7 +549,8 @@ typedef struct TriggerData
         DELETE 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 INSERT) or
-        skip the operation.
+        skip the operation.  For triggers on foreign tables, values of system
+        columns herein are unspecified.
        
       
      
@@ -563,7 +565,8 @@ typedef struct TriggerData
         DELETE. This is what you have to return
         from the function if the event is an UPDATE
         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.
        
       
      
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;