Triggers on foreign tables

Started by Ronan Dunklauover 12 years ago20 messages
#1Ronan Dunklau
rdunklau@gmail.com
1 attachment(s)

Hello.

I wanted to know what it would take to implement triggers on foreign tables.
It seems that statement-level triggers can work provided they are allowed in
the code.

Please find attached a simple POC patch that implement just that.

For row-level triggers, it seems more complicated. From what I understand,
OLD/NEW tuples are fetched from the heap using their ctid (except for BEFORE
INSERT triggers). How could this be adapted for foreign tables ?

--
Ronan Dunklau

Attachments:

foreign_triggers_v1.patchtext/x-patch; charset=utf-8; name=foreign_triggers_v1.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index e5ec738..08d133c 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -93,7 +93,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">
@@ -111,7 +111,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
       <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, views and foreign tables</entry>
      </row>
      <row>
       <entry align="center"><command>TRUNCATE</></entry>
@@ -122,7 +122,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
       <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, views and foreign tables</entry>
      </row>
      <row>
       <entry align="center"><command>TRUNCATE</></entry>
@@ -244,7 +244,7 @@ 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
+      The name (optionally schema-qualified) of the table, view or foreign table the trigger
       is for.
      </para>
     </listitem>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index f579340..37c0a35 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -33,7 +33,7 @@
    <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>
@@ -64,6 +64,15 @@
    </para>
 
    <para>
+    On foreign tables, trigger can be defined to execute either before or after any
+    <command>INSERT</command>,
+    <command>UPDATE</command> or
+    <command>DELETE</command> operation, only onece per <acronym>SQL statement.
+    This means that row-level triggers are not supported for foreign tables.
+   </para>
+
+
+   <para>
     The trigger function must be defined before the trigger itself can be
     created.  The trigger function must be declared as a
     function taking no arguments and returning type <literal>trigger</>.
@@ -96,6 +105,7 @@
     after may only be defined at statement level, while triggers that fire
     instead of an <command>INSERT</command>, <command>UPDATE</command>,
     or <command>DELETE</command> may only be defined at row level.
+    On foreign tables, triggers can not be defined at row level.
    </para>
 
    <para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index adc74dd..68bfa9c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3141,13 +3141,16 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			pass = AT_PASS_MISC;
 			break;
 		case AT_EnableTrig:		/* ENABLE TRIGGER variants */
-		case AT_EnableAlwaysTrig:
-		case AT_EnableReplicaTrig:
 		case AT_EnableTrigAll:
 		case AT_EnableTrigUser:
 		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_EnableReplicaTrig:
+		case AT_EnableAlwaysTrig:
 		case AT_EnableRule:		/* ENABLE/DISABLE RULE variants */
 		case AT_EnableAlwaysRule:
 		case AT_EnableReplicaRule:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index d86e9ad..d84b61b 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -184,12 +184,23 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 							RelationGetRelationName(rel)),
 					 errdetail("Views cannot have TRUNCATE triggers.")));
 	}
-	else
+	else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		if(stmt->row){
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("\"%s\" is a foreign table",
+							RelationGetRelationName(rel)),
+					 errdetail("Foreign tables cannot have row-level triggers.")));
+
+		}
+	}
+	else {
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is not a table or view",
 						RelationGetRelationName(rel))));
-
+    }
 	if (!allowSystemTableMods && IsSystemRelation(rel))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1062,10 +1073,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))
@@ -1166,10 +1178,11 @@ 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()))
@@ -1846,7 +1859,6 @@ ExecCallTriggerFunc(TriggerData *trigdata,
 							 InvalidOid, (Node *) trigdata, NULL);
 
 	pgstat_init_function_usage(&fcinfo, &fcusage);
-
 	MyTriggerDepth++;
 	PG_TRY();
 	{
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 60506e0..9d0ba8b 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -1158,6 +1158,24 @@ 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 INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger(); -- ERROR
+ERROR:  "foreign_table_1" is a foreign table
+DETAIL:  Foreign tables cannot have row-level triggers.
+CREATE TRIGGER trigtest BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest;
+DROP TRIGGER trigtest ON foreign_schema.foreign_table_1;
+DROP FUNCTION dummy_trigger();
 -- DROP FOREIGN TABLE
 DROP FOREIGN TABLE no_table;                                    -- ERROR
 ERROR:  foreign table "no_table" does not exist
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index f819eb1..74e6a40 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -470,6 +470,29 @@ 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 INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger(); -- ERROR
+
+CREATE TRIGGER trigtest BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest;
+
+DROP TRIGGER trigtest 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;
#2Michael Paquier
michael.paquier@gmail.com
In reply to: Ronan Dunklau (#1)
Re: Triggers on foreign tables

On Tue, Sep 10, 2013 at 5:08 AM, Ronan Dunklau <rdunklau@gmail.com> wrote:

Hello.

I wanted to know what it would take to implement triggers on foreign tables.
It seems that statement-level triggers can work provided they are allowed in
the code.

Please find attached a simple POC patch that implement just that.

For row-level triggers, it seems more complicated. From what I understand,
OLD/NEW tuples are fetched from the heap using their ctid (except for BEFORE
INSERT triggers). How could this be adapted for foreign tables ?

As your patch is targeting the implementation of a new feature, please
consider adding this patch to the next commit fest that is going to
begin in a couple of days:
https://commitfest.postgresql.org/action/commitfest_view?id=19

More general information on how patches are processed is findable here:
http://wiki.postgresql.org/wiki/Submitting_a_Patch
Regards,
--
Michael

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Ronan Dunklau
rdunklau@gmail.com
In reply to: Michael Paquier (#2)
Re: Triggers on foreign tables

On Wednesday 11 September 2013 06:27:24 Michael Paquier wrote:

As your patch is targeting the implementation of a new feature, please
consider adding this patch to the next commit fest that is going to
begin in a couple of days:
https://commitfest.postgresql.org/action/commitfest_view?id=19

I intended to do that in the next couple of days if I don't get any feedback.
Thank you for the reminder, its done.

#4Peter Eisentraut
peter_e@gmx.net
In reply to: Ronan Dunklau (#3)
Re: Triggers on foreign tables

On 9/11/13 10:14 AM, Ronan Dunklau wrote:

On Wednesday 11 September 2013 06:27:24 Michael Paquier wrote:

As your patch is targeting the implementation of a new feature, please
consider adding this patch to the next commit fest that is going to
begin in a couple of days:
https://commitfest.postgresql.org/action/commitfest_view?id=19

I intended to do that in the next couple of days if I don't get any feedback.
Thank you for the reminder, its done.

The documentation build fails:

openjade:trigger.sgml:72:9:E: end tag for "ACRONYM" omitted, but OMITTAG
NO was specified
openjade:trigger.sgml:70:56: start tag was here

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#5Ronan Dunklau
rdunklau@gmail.com
In reply to: Peter Eisentraut (#4)
1 attachment(s)
Re: Triggers on foreign tables

On Thursday 12 September 2013 12:10:01 Peter Eisentraut wrote:

The documentation build fails:

openjade:trigger.sgml:72:9:E: end tag for "ACRONYM" omitted, but OMITTAG
NO was specified
openjade:trigger.sgml:70:56: start tag was here

Thank you, I took the time to install a working doc-building environment.
Please find attached a patch that corrects this.

Attachments:

foreign_triggers_v2.patchtext/x-patch; charset=utf-8; name=foreign_triggers_v2.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index e5ec738..08d133c 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -93,7 +93,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">
@@ -111,7 +111,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
       <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, views and foreign tables</entry>
      </row>
      <row>
       <entry align="center"><command>TRUNCATE</></entry>
@@ -122,7 +122,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
       <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, views and foreign tables</entry>
      </row>
      <row>
       <entry align="center"><command>TRUNCATE</></entry>
@@ -244,7 +244,7 @@ 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
+      The name (optionally schema-qualified) of the table, view or foreign table the trigger
       is for.
      </para>
     </listitem>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index f579340..37c0a35 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -33,7 +33,7 @@
    <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>
@@ -64,6 +64,15 @@
    </para>
 
    <para>
+    On foreign tables, trigger can be defined to execute either before or after any
+    <command>INSERT</command>,
+    <command>UPDATE</command> or
+    <command>DELETE</command> operation, only once per <acronym>SQL</acronym> statement.
+    This means that row-level triggers are not supported for foreign tables.
+   </para>
+
+
+   <para>
     The trigger function must be defined before the trigger itself can be
     created.  The trigger function must be declared as a
     function taking no arguments and returning type <literal>trigger</>.
@@ -96,6 +105,7 @@
     after may only be defined at statement level, while triggers that fire
     instead of an <command>INSERT</command>, <command>UPDATE</command>,
     or <command>DELETE</command> may only be defined at row level.
+    On foreign tables, triggers can not be defined at row level.
    </para>
 
    <para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index adc74dd..68bfa9c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3141,13 +3141,16 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			pass = AT_PASS_MISC;
 			break;
 		case AT_EnableTrig:		/* ENABLE TRIGGER variants */
-		case AT_EnableAlwaysTrig:
-		case AT_EnableReplicaTrig:
 		case AT_EnableTrigAll:
 		case AT_EnableTrigUser:
 		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_EnableReplicaTrig:
+		case AT_EnableAlwaysTrig:
 		case AT_EnableRule:		/* ENABLE/DISABLE RULE variants */
 		case AT_EnableAlwaysRule:
 		case AT_EnableReplicaRule:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index d86e9ad..d84b61b 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -184,12 +184,23 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 							RelationGetRelationName(rel)),
 					 errdetail("Views cannot have TRUNCATE triggers.")));
 	}
-	else
+	else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		if(stmt->row){
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("\"%s\" is a foreign table",
+							RelationGetRelationName(rel)),
+					 errdetail("Foreign tables cannot have row-level triggers.")));
+
+		}
+	}
+	else {
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is not a table or view",
 						RelationGetRelationName(rel))));
-
+    }
 	if (!allowSystemTableMods && IsSystemRelation(rel))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1062,10 +1073,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))
@@ -1166,10 +1178,11 @@ 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()))
@@ -1846,7 +1859,6 @@ ExecCallTriggerFunc(TriggerData *trigdata,
 							 InvalidOid, (Node *) trigdata, NULL);
 
 	pgstat_init_function_usage(&fcinfo, &fcusage);
-
 	MyTriggerDepth++;
 	PG_TRY();
 	{
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 60506e0..9d0ba8b 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -1158,6 +1158,24 @@ 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 INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger(); -- ERROR
+ERROR:  "foreign_table_1" is a foreign table
+DETAIL:  Foreign tables cannot have row-level triggers.
+CREATE TRIGGER trigtest BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest;
+DROP TRIGGER trigtest ON foreign_schema.foreign_table_1;
+DROP FUNCTION dummy_trigger();
 -- DROP FOREIGN TABLE
 DROP FOREIGN TABLE no_table;                                    -- ERROR
 ERROR:  foreign table "no_table" does not exist
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index f819eb1..74e6a40 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -470,6 +470,29 @@ 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 INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH ROW
+EXECUTE PROCEDURE dummy_trigger(); -- ERROR
+
+CREATE TRIGGER trigtest BEFORE INSERT OR UPDATE OR DELETE ON foreign_schema.foreign_table_1
+FOR EACH STATEMENT
+EXECUTE PROCEDURE dummy_trigger();
+
+
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DISABLE TRIGGER trigtest;
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ENABLE TRIGGER trigtest;
+
+DROP TRIGGER trigtest 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;
#6Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Ronan Dunklau (#1)
Re: Triggers on foreign tables

2013/9/10 Ronan Dunklau <rdunklau@gmail.com>:

For row-level triggers, it seems more complicated. From what I understand,
OLD/NEW tuples are fetched from the heap using their ctid (except for BEFORE
INSERT triggers). How could this be adapted for foreign tables ?

It seems to me the origin of difficulty to support row-level trigger
is that foreign table
does not have a back-door to see the older tuple to be updated, unlike
regular tables.
The core-PostgreSQL knows on-disk format to store tuples within
regular tables, thus,
GetTupleForTrigger() can fetch a tuple being identified by tupleid.
On the other hand, writable foreign table is also designed to identify
a remote tuple
with tupleid, as long as FDW module is responsible.
So, a straightforward idea is adding a new FDW interface to get an
older image of
the tuple to be updated. It makes possible to implement something similar to
GetTupleForTrigger() on foreign tables, even though it takes a
secondary query to
fetch a record from the remote server. Probably, it is an headache for us.

One thing we pay attention is, the tuple to be updated is already
fetched from the
remote server and the tuple being fetched latest is (always?) the
target of update
or delete. It is not a heavy job for ForeignScanState node to hold a
copy of the latest
tuple. Isn't it an available way to reference relevant
ForeignScanState to get the older
image?
If my assumption is right, all we need to enhance are (1) add a store on
ForeignScanState to hold the last tuple on the scan stage, (2) add
GetForeignTupleForTrigger() to reference the store above on the relevant
ForeignScanState.

Any comment please.
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Jim Nasby
jim@nasby.net
In reply to: Kohei KaiGai (#6)
Re: Triggers on foreign tables

On 10/6/13 3:33 PM, Kohei KaiGai wrote:

2013/9/10 Ronan Dunklau <rdunklau@gmail.com>:

For row-level triggers, it seems more complicated. From what I understand,
OLD/NEW tuples are fetched from the heap using their ctid (except for BEFORE
INSERT triggers). How could this be adapted for foreign tables ?

It seems to me the origin of difficulty to support row-level trigger
is that foreign table
does not have a back-door to see the older tuple to be updated, unlike
regular tables.
The core-PostgreSQL knows on-disk format to store tuples within
regular tables, thus,
GetTupleForTrigger() can fetch a tuple being identified by tupleid.
On the other hand, writable foreign table is also designed to identify
a remote tuple
with tupleid, as long as FDW module is responsible.
So, a straightforward idea is adding a new FDW interface to get an
older image of
the tuple to be updated. It makes possible to implement something similar to
GetTupleForTrigger() on foreign tables, even though it takes a
secondary query to
fetch a record from the remote server. Probably, it is an headache for us.

One thing we pay attention is, the tuple to be updated is already
fetched from the
remote server and the tuple being fetched latest is (always?) the
target of update
or delete. It is not a heavy job for ForeignScanState node to hold a
copy of the latest
tuple. Isn't it an available way to reference relevant
ForeignScanState to get the older
image?
If my assumption is right, all we need to enhance are (1) add a store on
ForeignScanState to hold the last tuple on the scan stage, (2) add
GetForeignTupleForTrigger() to reference the store above on the relevant
ForeignScanState.

Any comment please.

What happens if someone changes the record on the foreign side between when we've read it and we do the UPDATE?
--
Jim C. Nasby, Data Architect jim@nasby.net
512.569.9461 (cell) http://jim.nasby.net

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#8Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Jim Nasby (#7)
Re: Triggers on foreign tables

What happens if someone changes the record on the foreign side between when
we've read it and we do the UPDATE?

Concurrency control is job of FDW driver. It has to coordinate access to
the records to be fetched for update / delete.
In fact, postgres_fdw add "FOR UPDATE" to avoid concurrent update
when it issues 1st-stage query to remote server.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#9Ronan Dunklau
rdunklau@gmail.com
In reply to: Kohei KaiGai (#6)
Re: Triggers on foreign tables

Le dimanche 6 octobre 2013 22:33:23 Kohei KaiGai a écrit :

2013/9/10 Ronan Dunklau <rdunklau@gmail.com>:

For row-level triggers, it seems more complicated. From what I understand,
OLD/NEW tuples are fetched from the heap using their ctid (except for
BEFORE INSERT triggers). How could this be adapted for foreign tables ?

It seems to me the origin of difficulty to support row-level trigger
is that foreign table
does not have a back-door to see the older tuple to be updated, unlike
regular tables.
The core-PostgreSQL knows on-disk format to store tuples within
regular tables, thus,
GetTupleForTrigger() can fetch a tuple being identified by tupleid.
On the other hand, writable foreign table is also designed to identify
a remote tuple
with tupleid, as long as FDW module is responsible.

It is my understanding that the tupleid is one way for a FDW to identify a
tuple, but the AddForeignUpdateTargets hook allows for arbitrary resjunks
columns to be added. It is these columns that are then used to identify the
tuple to update. I don't think the tupleid itself can be used to identify a
tuple for the trigger.

So, a straightforward idea is adding a new FDW interface to get an
older image of
the tuple to be updated. It makes possible to implement something similar to
GetTupleForTrigger() on foreign tables, even though it takes a
secondary query to
fetch a record from the remote server. Probably, it is an headache for us

One thing we pay attention is, the tuple to be updated is already
fetched from the
remote server and the tuple being fetched latest is (always?) the
target of update
or delete. It is not a heavy job for ForeignScanState node to hold a
copy of the latest
tuple. Isn't it an available way to reference relevant
ForeignScanState to get the older
image?

It is however possible to have only an incomplete tuple.

The FDW may have only fetched the tupleid-equivalent, and we would have to
ensure that the reltargetlist contains every attribute that the trigger could
need. Or, perform a second round-trip to the foreign data store to fetch the
whole tuple.

If my assumption is right, all we need to enhance are (1) add a store on
ForeignScanState to hold the last tuple on the scan stage, (2) add
GetForeignTupleForTrigger() to reference the store above on the relevant
ForeignScanState.

I would add a 3) have a GetTupleForTrigger() hook that would get called with
the stored tuple.

I personnally don't like this approach: there is already (n+1) round trips to
the foreign store (one during the scan phase, and one per tuple during the
update/delete phase), we don't need another one per tuple for the triggers.

#10Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Ronan Dunklau (#9)
Re: Triggers on foreign tables

2013/10/10 Ronan Dunklau <rdunklau@gmail.com>:

Le dimanche 6 octobre 2013 22:33:23 Kohei KaiGai a écrit :

2013/9/10 Ronan Dunklau <rdunklau@gmail.com>:

For row-level triggers, it seems more complicated. From what I understand,
OLD/NEW tuples are fetched from the heap using their ctid (except for
BEFORE INSERT triggers). How could this be adapted for foreign tables ?

It seems to me the origin of difficulty to support row-level trigger
is that foreign table
does not have a back-door to see the older tuple to be updated, unlike
regular tables.
The core-PostgreSQL knows on-disk format to store tuples within
regular tables, thus,
GetTupleForTrigger() can fetch a tuple being identified by tupleid.
On the other hand, writable foreign table is also designed to identify
a remote tuple
with tupleid, as long as FDW module is responsible.

It is my understanding that the tupleid is one way for a FDW to identify a
tuple, but the AddForeignUpdateTargets hook allows for arbitrary resjunks
columns to be added. It is these columns that are then used to identify the
tuple to update. I don't think the tupleid itself can be used to identify a
tuple for the trigger.

Sorry, I'm uncertain the point above.
Are you saying FDW driver may be able to handle well a case when
a remote tuple to be updated is different from a remote tuple being
fetched on the first stage? Or, am I misunderstanding?

From another standpoint, I'd like to cancel the above my suggestion,
because after-row update or delete trigger tries to fetch an older image
of tuple next to the actual update / delete jobs.
Even if FDW is responsible to identify a remote tuple using tupleid,
the identified tuple we can fetch is the newer image because FDW
driver already issues a remote query to update or delete the target
record.
So, soon or later, FDW shall be responsible to keep a complete tuple
image when foreign table has row-level triggers, until writer operation
is finished.

So, a straightforward idea is adding a new FDW interface to get an
older image of
the tuple to be updated. It makes possible to implement something similar to
GetTupleForTrigger() on foreign tables, even though it takes a
secondary query to
fetch a record from the remote server. Probably, it is an headache for us

One thing we pay attention is, the tuple to be updated is already
fetched from the
remote server and the tuple being fetched latest is (always?) the
target of update
or delete. It is not a heavy job for ForeignScanState node to hold a
copy of the latest
tuple. Isn't it an available way to reference relevant
ForeignScanState to get the older
image?

It is however possible to have only an incomplete tuple.

The FDW may have only fetched the tupleid-equivalent, and we would have to
ensure that the reltargetlist contains every attribute that the trigger could
need.

Does the incomplete tuple mean a tuple image but some of columns
are replaced with NULL due to optimization, as postgres_fdw is doing,
doesn't it?
If so, a solution is to enforce FDW driver to fetch all the columns if its
managing foreign table has row level triggers for update / delete.

Or, perform a second round-trip to the foreign data store to fetch the
whole tuple.

It might be a solution for before-row trigger, but not for after-row trigger,
unfortunately.

If my assumption is right, all we need to enhance are (1) add a store on
ForeignScanState to hold the last tuple on the scan stage, (2) add
GetForeignTupleForTrigger() to reference the store above on the relevant
ForeignScanState.

I would add a 3) have a GetTupleForTrigger() hook that would get called with
the stored tuple.

I personnally don't like this approach: there is already (n+1) round trips to
the foreign store (one during the scan phase, and one per tuple during the
update/delete phase), we don't need another one per tuple for the triggers.

As I noted above, 2nd round trip is not a suitable design to support after-row
trigger. Likely, GetTupleForTrigger() hook shall perform to return a cached
older image being fetched on the first round of remote scan.
So, I guess every FDW driver will have similar implementation to handle
these the situation, thus it makes sense that PostgreSQL core has
a feature to support this handling; that keeps a tuple being fetched last
and returns older image for row-level triggers.

How about your opinion?

And, I also want some comments from committers, not only from mine.
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11Ronan Dunklau
rdunklau@gmail.com
In reply to: Kohei KaiGai (#10)
Re: Triggers on foreign tables

Le samedi 12 octobre 2013 07:30:35 Kohei KaiGai a écrit :

2013/10/10 Ronan Dunklau <rdunklau@gmail.com>:

Sorry, I'm uncertain the point above.
Are you saying FDW driver may be able to handle well a case when
a remote tuple to be updated is different from a remote tuple being
fetched on the first stage? Or, am I misunderstanding?

I was not clear in the point above: what I meant was that, from my
understanding, the FDW driver has no obligation to use the system-attribute
"tupleid" to identify the remote tuple. IIRC, one of the early API proposal
involved had the tupleid passed as an argument to the ExecForeign* hooks.
Now, these hooks receive the whole "planslot" instead. This slot can store
additional resjunks attributes (thanks to the "AddForeignUpdateTargets")
which shouldn't be altered during the execution and are carried on until
modify stage. So, these additional attributes can be used as identifiers
instead of the tupleid.

In fact, this is the approach I implemented for the multicorn fdw, and I
believe it to be used in other FDWs as well (HadoopFDW does that, if I
understand it correctly).

So, it doesn't make sense to me to assume that the tupleid will be filled as
valid remote identifier in the context of triggers.

From another standpoint, I'd like to cancel the above my suggestion,
because after-row update or delete trigger tries to fetch an older image
of tuple next to the actual update / delete jobs.
Even if FDW is responsible to identify a remote tuple using tupleid,
the identified tuple we can fetch is the newer image because FDW
driver already issues a remote query to update or delete the target
record.
So, soon or later, FDW shall be responsible to keep a complete tuple
image when foreign table has row-level triggers, until writer operation
is finished.

+1

Does the incomplete tuple mean a tuple image but some of columns
are replaced with NULL due to optimization, as postgres_fdw is doing,
doesn't it?
If so, a solution is to enforce FDW driver to fetch all the columns if its
managing foreign table has row level triggers for update / delete.

What I meant is that a DELETE statement, for example, does not build a scan-
stage reltargetlist consisting of the whole set of attributes: the only
attributes that are fetched are the ones built by addForeignUpdateTargets, and
whatever additional attributes are involved in the DELETE statement (in the
WHERE clause, for example).

I apologize if this is not clear, since both my technical and english
vocabulary are maybe not precise enough to get my point accross.

Or, perform a second round-trip to the foreign data store to fetch the
whole tuple.

It might be a solution for before-row trigger, but not for after-row
trigger, unfortunately.

+1

As I noted above, 2nd round trip is not a suitable design to support
after-row trigger. Likely, GetTupleForTrigger() hook shall perform to
return a cached older image being fetched on the first round of remote
scan.
So, I guess every FDW driver will have similar implementation to handle
these the situation, thus it makes sense that PostgreSQL core has
a feature to support this handling; that keeps a tuple being fetched last
and returns older image for row-level triggers.

How about your opinion?

I like this idea, but I don't really see all the implications of such a
design.
Where would this tuple be stored ?
From what I understand, after-triggers are queued for later execution, using
the old/new tupleid for later retrieval (in the AfterTriggerEventData struct).
This would need a major change to work with foreign tables. Would that involve
a special path for executing those triggers ASAP ?

And, I also want some comments from committers, not only from mine.

+1

Thank you for taking the time to think this through.

--
Ronan Dunklau

#12Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Ronan Dunklau (#11)
Re: Triggers on foreign tables

2013/10/13 Ronan Dunklau <rdunklau@gmail.com>:

Le samedi 12 octobre 2013 07:30:35 Kohei KaiGai a écrit :

2013/10/10 Ronan Dunklau <rdunklau@gmail.com>:

Sorry, I'm uncertain the point above.
Are you saying FDW driver may be able to handle well a case when
a remote tuple to be updated is different from a remote tuple being
fetched on the first stage? Or, am I misunderstanding?

I was not clear in the point above: what I meant was that, from my
understanding, the FDW driver has no obligation to use the system-attribute
"tupleid" to identify the remote tuple. IIRC, one of the early API proposal
involved had the tupleid passed as an argument to the ExecForeign* hooks.
Now, these hooks receive the whole "planslot" instead. This slot can store
additional resjunks attributes (thanks to the "AddForeignUpdateTargets")
which shouldn't be altered during the execution and are carried on until
modify stage. So, these additional attributes can be used as identifiers
instead of the tupleid.

In fact, this is the approach I implemented for the multicorn fdw, and I
believe it to be used in other FDWs as well (HadoopFDW does that, if I
understand it correctly).

So, it doesn't make sense to me to assume that the tupleid will be filled as
valid remote identifier in the context of triggers.

Sorry, I might call it something like primary key, instead of 'tupleid'.
Apart from whether we can uniquely identify a particular remote record with
an attribute, what FDW needs here is "something to identify a remote record".
So, we were talking about same concept with different names.

Does the incomplete tuple mean a tuple image but some of columns
are replaced with NULL due to optimization, as postgres_fdw is doing,
doesn't it?
If so, a solution is to enforce FDW driver to fetch all the columns if its
managing foreign table has row level triggers for update / delete.

What I meant is that a DELETE statement, for example, does not build a scan-
stage reltargetlist consisting of the whole set of attributes: the only
attributes that are fetched are the ones built by addForeignUpdateTargets, and
whatever additional attributes are involved in the DELETE statement (in the
WHERE clause, for example).

DELETE statement indeed does not (need to) construct a complete tuple
image on scan stage, however, it does not prohibit FDW driver to keep the
latest record being fetched from remote server.
FDW driver easily knows whether relation has row-level trigger preliminary,
so here is no matter to keep a complete image internally if needed.
Or, it is perhaps available to add additional target-entries that is
needed to construct a complete tuple using AddForeignUpdateTargets()
callback.

As I noted above, 2nd round trip is not a suitable design to support
after-row trigger. Likely, GetTupleForTrigger() hook shall perform to
return a cached older image being fetched on the first round of remote
scan.
So, I guess every FDW driver will have similar implementation to handle
these the situation, thus it makes sense that PostgreSQL core has
a feature to support this handling; that keeps a tuple being fetched last
and returns older image for row-level triggers.

How about your opinion?

I like this idea, but I don't really see all the implications of such a
design.
Where would this tuple be stored ?
From what I understand, after-triggers are queued for later execution, using
the old/new tupleid for later retrieval (in the AfterTriggerEventData struct).
This would need a major change to work with foreign tables. Would that involve
a special path for executing those triggers ASAP ?

Ops, I oversight here. AfterTriggerSaveEvent() indeed saves only ctid of
tuples in regular tables, because it can re-construct a complete tuple
image from a particular ctid any time.
On the other hand, it is not available on foreign table due to its nature.
All we can do seems to me, probably, to save copy of before/after tuple
image on local memory, even though it consumes much memory than
regular tables.

The feature we need here might become a bit clear little by little.
A complete tuple image shall be fetched on the scan stage, if foreign
table has row-level trigger. The planSlot may make sense if it contains
(junk) attributes that is sufficient to re-construct an old tuple image.
Target-list of DELETE statement contains a junk ctid attribute. Here
is no reason why we cannot add junk attributes that reference each
regular attribute, isn't it? Also, target-list of UPDATE statement
contains new tuple image, however, it is available to add junk attributes
that just reference old value, as a carrier of old values from scan stage
to modify stage.
I want core PostgreSQL to support a part of these jobs. For example,
it may be able to add junk attribute to re-construct old tuple image on
rewriteTargetListUD(), if target relation has row-level triggers. Also, it
may be able to extract these old values from planSlot to construct
an old tuple image on GetTupleForTrigger(), if relation is foreign table.
Unfortunately, I have no special idea to handle old/new remote tuple
on AfterTriggerSaveEvent(), except for keeping it as copy, but it is
straightforward.

And, I also want some comments from committers, not only from mine.

+1

+1

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#13Robert Haas
robertmhaas@gmail.com
In reply to: Kohei KaiGai (#12)
Re: Triggers on foreign tables

On Mon, Oct 14, 2013 at 5:24 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

And, I also want some comments from committers, not only from mine.

+1

+1

/me pokes head up. I know I'm going to annoy people with this
comment, but I feel like it's going to have to be made at some point
by somebody, so here goes: I don't see the point of this feature. If
you want a trigger on a table, why not set it on the remote side? A
trigger on the foreign table won't be enforced consistently; it'll
only work when the update is routed through the foreign table, not
when people access the underlying table on the remote side through any
other mechanism. The number of useful things you can do this way
seems fairly small. Perhaps you could use a row-level trigger for
RLS, to allow only certain rows on the foreign side to be updated, but
even that seems like a slightly strange design: generally it'd be
better to enforce the security as close to the target object as
possible.

There's another issue that concerns me here also: performance. IIUC,
an update of N tuples on the remote side currently requires N+1 server
round-trips. That is unspeakably awful, and we really oughta be
looking for ways to make that number go down, by pushing the whole
update to the remote side. But obviously that won't be possible if
there's a per-row trigger that has to be evaluated on the local side.
Now, assuming somebody comes up with code that implements that
optimization, we can just disable it when there are local-side
triggers. But, then you're back to having terrible performance. So
even if the use case for this seemed really broad, I tend to think
performance concerns would sink most of the possible real-world uses.

I could, of course, be all wet....

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#14Stephen Frost
sfrost@snowman.net
In reply to: Robert Haas (#13)
Re: Triggers on foreign tables

Robert,

* Robert Haas (robertmhaas@gmail.com) wrote:

/me pokes head up. I know I'm going to annoy people with this
comment, but I feel like it's going to have to be made at some point

Perhaps some folks will be annoyed- I'm not annoyed, but I don't
really agree. :)

by somebody, so here goes: I don't see the point of this feature. If
you want a trigger on a table, why not set it on the remote side? A
trigger on the foreign table won't be enforced consistently; it'll
only work when the update is routed through the foreign table, not
when people access the underlying table on the remote side through any
other mechanism. The number of useful things you can do this way
seems fairly small. Perhaps you could use a row-level trigger for
RLS, to allow only certain rows on the foreign side to be updated, but
even that seems like a slightly strange design: generally it'd be
better to enforce the security as close to the target object as
possible.

I can certainly see use-cases for this, a very simple one being a way to
keep track of what's been updated/inserted/whatever through this
particular foreign table (essentially, an auditing table). The *remote*
side might not be ideal for tracking that information and you might want
the info locally and remotely anyway.

There's another issue that concerns me here also: performance. IIUC,
an update of N tuples on the remote side currently requires N+1 server
round-trips. That is unspeakably awful, and we really oughta be
looking for ways to make that number go down, by pushing the whole
update to the remote side. But obviously that won't be possible if
there's a per-row trigger that has to be evaluated on the local side.
Now, assuming somebody comes up with code that implements that
optimization, we can just disable it when there are local-side
triggers. But, then you're back to having terrible performance. So
even if the use case for this seemed really broad, I tend to think
performance concerns would sink most of the possible real-world uses.

Performance, while a concern, should probably be secondary when there
are valid use-cases for this where the performance wouldn't be a problem
for users.

Thanks,

Stephen

#15Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Robert Haas (#13)
Re: Triggers on foreign tables

2013/10/15 Robert Haas <robertmhaas@gmail.com>:

On Mon, Oct 14, 2013 at 5:24 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

And, I also want some comments from committers, not only from mine.

+1

+1

/me pokes head up. I know I'm going to annoy people with this
comment, but I feel like it's going to have to be made at some point
by somebody, so here goes: I don't see the point of this feature. If
you want a trigger on a table, why not set it on the remote side? A
trigger on the foreign table won't be enforced consistently; it'll
only work when the update is routed through the foreign table, not
when people access the underlying table on the remote side through any
other mechanism. The number of useful things you can do this way
seems fairly small. Perhaps you could use a row-level trigger for
RLS, to allow only certain rows on the foreign side to be updated, but
even that seems like a slightly strange design: generally it'd be
better to enforce the security as close to the target object as
possible.

One reason we should support local triggers is that not all the data
source of FDW support remote trigger. It required FDW drivers to
have RDBMS as its backend, but no realistic assumption.
For example, file_fdw is unavailable to implement remote triggers.

One thing I'd like to know is, where is the goal of FDW feature.
It seems to me, FDW goes into a feature to manage external data
set as if regular tables. If it is right understanding, things we try to
support on foreign table is things we're supporting on regular tables,
such as triggers.

There's another issue that concerns me here also: performance. IIUC,
an update of N tuples on the remote side currently requires N+1 server
round-trips. That is unspeakably awful, and we really oughta be
looking for ways to make that number go down, by pushing the whole
update to the remote side. But obviously that won't be possible if
there's a per-row trigger that has to be evaluated on the local side.
Now, assuming somebody comes up with code that implements that
optimization, we can just disable it when there are local-side
triggers. But, then you're back to having terrible performance. So
even if the use case for this seemed really broad, I tend to think
performance concerns would sink most of the possible real-world uses.

We often have some case that we cannot apply fully optimized path
because of some reasons, like view has security-barrier, qualifier
contained volatile functions, and so on...
Trigger may be a factor to prevent fully optimized path, however,
it depends on the situation which one shall be prioritized; performance
or functionality.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#16Ronan Dunklau
rdunklau@gmail.com
In reply to: Robert Haas (#13)
Re: Triggers on foreign tables

Le mardi 15 octobre 2013 09:47:31 Robert Haas a écrit :

On Mon, Oct 14, 2013 at 5:24 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

And, I also want some comments from committers, not only from mine.

+1

+1

/me pokes head up. I know I'm going to annoy people with this
comment, but I feel like it's going to have to be made at some point
by somebody, so here goes: I don't see the point of this feature. If
you want a trigger on a table, why not set it on the remote side? A
trigger on the foreign table won't be enforced consistently; it'll
only work when the update is routed through the foreign table, not
when people access the underlying table on the remote side through any
other mechanism. The number of useful things you can do this way
seems fairly small. Perhaps you could use a row-level trigger for
RLS, to allow only certain rows on the foreign side to be updated, but
even that seems like a slightly strange design: generally it'd be
better to enforce the security as close to the target object as
possible.

There's another issue that concerns me here also: performance. IIUC,
an update of N tuples on the remote side currently requires N+1 server
round-trips. That is unspeakably awful, and we really oughta be
looking for ways to make that number go down, by pushing the whole
update to the remote side. But obviously that won't be possible if
there's a per-row trigger that has to be evaluated on the local side.
Now, assuming somebody comes up with code that implements that
optimization, we can just disable it when there are local-side
triggers. But, then you're back to having terrible performance. So
even if the use case for this seemed really broad, I tend to think
performance concerns would sink most of the possible real-world uses.

I could, of course, be all wet....

Even if the row-level trigger functionality is controversial, in terms of
provided features VS performance, the original patch I submitted enables
statement-level triggers.

Although my goal was to implement row-level triggers and I hit a wall pretty
quickly, would it make sense to have statement-level triggers without their
row counterparts ?

I also think that pushing the whole update to the remote side will not be
possible in all cases, and like Kohei wrote, it may be an acceptable loss to
not be able to push it when a trigger is present. If we want to push the whole
update, we will have to cope with local joins and function evaluations in all
cases. IMO, triggers are just a special case of these limiting factors.

#17Ronan Dunklau
rdunklau@gmail.com
In reply to: Kohei KaiGai (#12)
Re: Triggers on foreign tables

Sorry, I might call it something like primary key, instead of 'tupleid'.
Apart from whether we can uniquely identify a particular remote record with
an attribute, what FDW needs here is "something to identify a remote
record". So, we were talking about same concept with different names.

Ah, that makes sense: I was understanding tupleid as a synonym for ctid.

Does the incomplete tuple mean a tuple image but some of columns
are replaced with NULL due to optimization, as postgres_fdw is doing,
doesn't it?
If so, a solution is to enforce FDW driver to fetch all the columns if
its
managing foreign table has row level triggers for update / delete.

What I meant is that a DELETE statement, for example, does not build a
scan- stage reltargetlist consisting of the whole set of attributes: the
only attributes that are fetched are the ones built by
addForeignUpdateTargets, and whatever additional attributes are involved
in the DELETE statement (in the WHERE clause, for example).

DELETE statement indeed does not (need to) construct a complete tuple
image on scan stage, however, it does not prohibit FDW driver to keep the
latest record being fetched from remote server.
FDW driver easily knows whether relation has row-level trigger preliminary,
so here is no matter to keep a complete image internally if needed.
Or, it is perhaps available to add additional target-entries that is
needed to construct a complete tuple using AddForeignUpdateTargets()
callback.

I think that the additional target-entries should be added in core, because
that would require additional work from FDW drivers, and the code would be
duplicated amongst all FDW drivers that wish to support triggers.

I like this idea, but I don't really see all the implications of such a
design.
Where would this tuple be stored ?
From what I understand, after-triggers are queued for later execution,
using the old/new tupleid for later retrieval (in the
AfterTriggerEventData struct). This would need a major change to work
with foreign tables. Would that involve a special path for executing
those triggers ASAP ?

Ops, I oversight here. AfterTriggerSaveEvent() indeed saves only ctid of
tuples in regular tables, because it can re-construct a complete tuple
image from a particular ctid any time.
On the other hand, it is not available on foreign table due to its nature.
All we can do seems to me, probably, to save copy of before/after tuple
image on local memory, even though it consumes much memory than
regular tables.

Or, as suggested above, add a special code path for foreign tables which would
execute the trigger as soon as possible instead of queuing it.

The feature we need here might become a bit clear little by little.
A complete tuple image shall be fetched on the scan stage, if foreign
table has row-level trigger. The planSlot may make sense if it contains
(junk) attributes that is sufficient to re-construct an old tuple image.
Target-list of DELETE statement contains a junk ctid attribute. Here
is no reason why we cannot add junk attributes that reference each
regular attribute, isn't it? Also, target-list of UPDATE statement
contains new tuple image, however, it is available to add junk attributes
that just reference old value, as a carrier of old values from scan stage
to modify stage.
I want core PostgreSQL to support a part of these jobs. For example,
it may be able to add junk attribute to re-construct old tuple image on
rewriteTargetListUD(), if target relation has row-level triggers. Also, it
may be able to extract these old values from planSlot to construct
an old tuple image on GetTupleForTrigger(), if relation is foreign table.

So, if I understand you, the target list would be built as follow:

- core builds the target list, including every attribute
- this target list is updated by the FDW to add any junk attributes it wishes
to use
- in the case of an UPDATE, core duplicates the whole list of attributes
(including the added junk attributes), as another set of junk attributes.
Maybe we could duplicate only the updated attributes.

Unfortunately, I have no special idea to handle old/new remote tuple
on AfterTriggerSaveEvent(), except for keeping it as copy, but it is
straightforward.

Maybe avoid it altogether in the case of a trigger on a remote foreign table ?

Show quoted text

And, I also want some comments from committers, not only from mine.

+1

+1

Thanks,

#18Robert Haas
robertmhaas@gmail.com
In reply to: Kohei KaiGai (#15)
Re: Triggers on foreign tables

On Tue, Oct 15, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

One reason we should support local triggers is that not all the data
source of FDW support remote trigger. It required FDW drivers to
have RDBMS as its backend, but no realistic assumption.
For example, file_fdw is unavailable to implement remote triggers.

True, but gosh, updates via file_fdw are gonna be so slow I can't
believe anybody'd use it for something real....

One thing I'd like to know is, where is the goal of FDW feature.
It seems to me, FDW goes into a feature to manage external data
set as if regular tables. If it is right understanding, things we try to
support on foreign table is things we're supporting on regular tables,
such as triggers.

I generally agree with that.

We often have some case that we cannot apply fully optimized path
because of some reasons, like view has security-barrier, qualifier
contained volatile functions, and so on...
Trigger may be a factor to prevent fully optimized path, however,
it depends on the situation which one shall be prioritized; performance
or functionality.

Sure. I mean, I guess if there are enough people that want this, I
suppose I ought not stand in the way. It just seems like a lot of
work for a feature of very marginal utility.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#19Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Robert Haas (#18)
Re: Triggers on foreign tables

2013/10/16 Robert Haas <robertmhaas@gmail.com>:

On Tue, Oct 15, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

One reason we should support local triggers is that not all the data
source of FDW support remote trigger. It required FDW drivers to
have RDBMS as its backend, but no realistic assumption.
For example, file_fdw is unavailable to implement remote triggers.

True, but gosh, updates via file_fdw are gonna be so slow I can't
believe anybody'd use it for something real....

How about another example? I have implemented a column-oriented
data store with read/write capability, using FDW APIs.
The role of FDW driver here is to translate its data format between
column-oriented and row-oriented, but no trigger capability itself.

One thing I'd like to know is, where is the goal of FDW feature.
It seems to me, FDW goes into a feature to manage external data
set as if regular tables. If it is right understanding, things we try to
support on foreign table is things we're supporting on regular tables,
such as triggers.

I generally agree with that.

We often have some case that we cannot apply fully optimized path
because of some reasons, like view has security-barrier, qualifier
contained volatile functions, and so on...
Trigger may be a factor to prevent fully optimized path, however,
it depends on the situation which one shall be prioritized; performance
or functionality.

Sure. I mean, I guess if there are enough people that want this, I
suppose I ought not stand in the way. It just seems like a lot of
work for a feature of very marginal utility.

The reason why I'm interested in this feature is, row-level triggers on
foreign tables will become a piece to implement table partitioning
that contains multiple foreign tables.
Probably, it also connects to the effort of custom-plan node (in the
future) that enables to replace Append node by custom logic, to kick
multiple concurrent remote queries on behalf of partitioned foreign table.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#20Robert Haas
robertmhaas@gmail.com
In reply to: Kohei KaiGai (#19)
Re: Triggers on foreign tables

On Wed, Oct 16, 2013 at 2:20 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

True, but gosh, updates via file_fdw are gonna be so slow I can't
believe anybody'd use it for something real....

How about another example? I have implemented a column-oriented
data store with read/write capability, using FDW APIs.
The role of FDW driver here is to translate its data format between
column-oriented and row-oriented, but no trigger capability itself.

OK, that's a believable example. Call me convinced. :-)

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers