PL/pgSQL return value in after triggers

Started by Peter Eisentrautalmost 15 years ago7 messages
#1Peter Eisentraut
peter_e@gmx.net

PL/pgSQL trigger functions currently require a value to be returned,
even though that value is not used for anything in case of a trigger
fired AFTER. I was wondering if we could relax that. It would make
things a bit more robust and produce clearer PL/pgSQL code. The
specific case I'm concerned about is that a trigger function could
accidentally be run in a BEFORE trigger even though it was not meant for
that. It is common practice that trigger functions for AFTER triggers
return NULL, which would have unpleasant effects if used in a BEFORE
trigger.

I think it is very uncommon to have the same function usable for BEFORE
and AFTER triggers, so it would be valuable to have coding support
specifically for AFTER triggers. We could just allow RETURN without
argument, or perhaps no RETURN at all.

Comments?

#2Robert Haas
robertmhaas@gmail.com
In reply to: Peter Eisentraut (#1)
Re: PL/pgSQL return value in after triggers

On Mon, Feb 28, 2011 at 12:07 PM, Peter Eisentraut <peter_e@gmx.net> wrote:

PL/pgSQL trigger functions currently require a value to be returned,
even though that value is not used for anything in case of a trigger
fired AFTER.  I was wondering if we could relax that.  It would make
things a bit more robust and produce clearer PL/pgSQL code.  The
specific case I'm concerned about is that a trigger function could
accidentally be run in a BEFORE trigger even though it was not meant for
that.  It is common practice that trigger functions for AFTER triggers
return NULL, which would have unpleasant effects if used in a BEFORE
trigger.

I think it is very uncommon to have the same function usable for BEFORE
and AFTER triggers, so it would be valuable to have coding support
specifically for AFTER triggers.  We could just allow RETURN without
argument, or perhaps no RETURN at all.

Comments?

It has bugged me for years that after triggers need to contain a
useless RETURN statement, but I'm not sure now is the time to go fix
it.

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

#3Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Eisentraut (#1)
Re: PL/pgSQL return value in after triggers

Peter Eisentraut <peter_e@gmx.net> writes:

PL/pgSQL trigger functions currently require a value to be returned,
even though that value is not used for anything in case of a trigger
fired AFTER. I was wondering if we could relax that.

I got bit by that just a couple days ago --- I supposed that a trigger
that wasn't returning anything useful shouldn't need an explicit
RETURN. So +1 for doing something about it. However, unless it's a
very small and simple patch, I concur with Robert that it might be
a bit late to consider this for 9.1.

regards, tom lane

#4Peter Eisentraut
peter_e@gmx.net
In reply to: Peter Eisentraut (#1)
1 attachment(s)
Re: PL/pgSQL return value in after triggers

On mån, 2011-02-28 at 19:07 +0200, Peter Eisentraut wrote:

PL/pgSQL trigger functions currently require a value to be returned,
even though that value is not used for anything in case of a trigger
fired AFTER. I was wondering if we could relax that. It would make
things a bit more robust and produce clearer PL/pgSQL code. The
specific case I'm concerned about is that a trigger function could
accidentally be run in a BEFORE trigger even though it was not meant for
that. It is common practice that trigger functions for AFTER triggers
return NULL, which would have unpleasant effects if used in a BEFORE
trigger.

I think it is very uncommon to have the same function usable for BEFORE
and AFTER triggers, so it would be valuable to have coding support
specifically for AFTER triggers. We could just allow RETURN without
argument, or perhaps no RETURN at all.

Here is a patch for that.

One thing that I'm concerned about with this is that it treats a plain
RETURN in a BEFORE trigger as RETURN NULL, whereas arguably it should be
an error. I haven't found a good way to handle that yet, but I'll keep
looking.

Attachments:

plpgsql-trigger-return.patchtext/x-patch; charset=UTF-8; name=plpgsql-trigger-return.patchDownload
diff --git i/doc/src/sgml/plpgsql.sgml w/doc/src/sgml/plpgsql.sgml
index f33cef5..203cb29 100644
--- i/doc/src/sgml/plpgsql.sgml
+++ w/doc/src/sgml/plpgsql.sgml
@@ -1559,7 +1559,7 @@ END;
      <title><command>RETURN</></title>
 
 <synopsis>
-RETURN <replaceable>expression</replaceable>;
+RETURN <optional> <replaceable>expression</replaceable> </optional>;
 </synopsis>
 
      <para>
@@ -1592,6 +1592,13 @@ RETURN <replaceable>expression</replaceable>;
      </para>
 
      <para>
+      A trigger function used in an <literal>AFTER</literal> row trigger or in
+      a statement trigger can also use <command>RETURN</command> without
+      expression.  (The result of any supplied expression would be ignored.)
+      See <xref linkend="plpgsql-trigger"> for details.
+     </para>
+
+     <para>
       The return value of a function cannot be left undefined. If
       control reaches the end of the top-level block of the function
       without hitting a <command>RETURN</command> statement, a run-time
@@ -3511,9 +3518,9 @@ RAISE unique_violation USING MESSAGE = 'Duplicate user ID: ' || user_id;
   </para>
 
    <para>
-    A trigger function must return either <symbol>NULL</symbol> or a
+    A trigger function can return either <symbol>NULL</symbol> or a
     record/row value having exactly the structure of the table the
-    trigger was fired for.
+    trigger was fired for, as follows.
    </para>
 
    <para>
@@ -3558,11 +3565,17 @@ RAISE unique_violation USING MESSAGE = 'Duplicate user ID: ' || user_id;
    </para>
 
    <para>
-    The return value of a row-level trigger
-    fired <literal>AFTER</literal> or a statement-level trigger
-    fired <literal>BEFORE</> or <literal>AFTER</> is
-    always ignored; it might as well be null. However, any of these types of
-    triggers might still abort the entire operation by raising an error.
+    The return value of a row-level trigger fired <literal>AFTER</literal> or a
+    statement-level trigger fired <literal>BEFORE</> or <literal>AFTER</> is
+    always ignored.  In these cases, the <literal>RETURN</literal> statement
+    can be used without argument or omitted altogether.  (A trigger function
+    that is intended to be used in an <literal>AFTER</literal> trigger or a
+    statement-level trigger and supplies a return value that is ignored would
+    misbehave, possibly silently, if accidentally used in
+    a <literal>BEFORE</literal> or <literal>INSTEAD OF</literal> row-level
+    trigger.  So it might be better not to supply a dummy return value.)
+    However, any of these types of triggers might still abort the entire
+    operation by raising an error.
    </para>
 
    <para>
@@ -3655,15 +3668,11 @@ CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$
         --
         IF (TG_OP = 'DELETE') THEN
             INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*;
-            RETURN OLD;
         ELSIF (TG_OP = 'UPDATE') THEN
             INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;
-            RETURN NEW;
         ELSIF (TG_OP = 'INSERT') THEN
             INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;
-            RETURN NEW;
         END IF;
-        RETURN NULL; -- result is ignored since this is an AFTER trigger
     END;
 $emp_audit$ LANGUAGE plpgsql;
 
@@ -3885,9 +3894,6 @@ AS $maint_sales_summary_bytime$
                     -- do nothing
             END;
         END LOOP insert_update;
-
-        RETURN NULL;
-
     END;
 $maint_sales_summary_bytime$ LANGUAGE plpgsql;
 
diff --git i/src/pl/plpgsql/src/gram.y w/src/pl/plpgsql/src/gram.y
index 8c4c2f7..a7adb5c 100644
--- i/src/pl/plpgsql/src/gram.y
+++ w/src/pl/plpgsql/src/gram.y
@@ -2868,6 +2868,8 @@ make_return_stmt(int location)
 	}
 	else if (plpgsql_curr_compile->fn_retistuple)
 	{
+		bool saw_semicolon = false;
+
 		switch (yylex())
 		{
 			case K_NULL:
@@ -2885,6 +2887,16 @@ make_return_stmt(int location)
 							 parser_errposition(yylloc)));
 				break;
 
+			case ';':
+				if (plpgsql_curr_compile->fn_is_trigger)
+					saw_semicolon = true;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("RETURN must specify a record or row variable in function returning row"),
+							 parser_errposition(yylloc)));
+				break;
+
 			default:
 				ereport(ERROR,
 						(errcode(ERRCODE_DATATYPE_MISMATCH),
@@ -2892,7 +2904,7 @@ make_return_stmt(int location)
 						 parser_errposition(yylloc)));
 				break;
 		}
-		if (yylex() != ';')
+		if (!saw_semicolon && yylex() != ';')
 			yyerror("syntax error");
 	}
 	else
diff --git i/src/pl/plpgsql/src/pl_exec.c w/src/pl/plpgsql/src/pl_exec.c
index 717ad79..2236f72 100644
--- i/src/pl/plpgsql/src/pl_exec.c
+++ w/src/pl/plpgsql/src/pl_exec.c
@@ -701,10 +701,11 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
 					 errmsg("CONTINUE cannot be used outside a loop")));
-		else
+		else if (!(TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event) || TRIGGER_FIRED_AFTER(trigdata->tg_event)))
 			ereport(ERROR,
 			   (errcode(ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT),
-				errmsg("control reached end of trigger procedure without RETURN")));
+				errmsg("control reached end of trigger procedure without RETURN"),
+				errdetail("A trigger procedure used in a row-level trigger that is not fired AFTER must return a value.")));
 	}
 
 	estate.err_stmt = NULL;
diff --git i/src/test/regress/expected/plpgsql.out w/src/test/regress/expected/plpgsql.out
index 238bf5f..cda9928 100644
--- i/src/test/regress/expected/plpgsql.out
+++ w/src/test/regress/expected/plpgsql.out
@@ -4571,3 +4571,49 @@ ERROR:  value for domain orderedarray violates check constraint "sorted"
 CONTEXT:  PL/pgSQL function "testoa" line 5 at assignment
 drop function arrayassign1();
 drop function testoa(x1 int, x2 int, x3 int);
+--
+-- Additional triggers tests
+--
+create table test_triggers (a int, b text);
+-- trigger function without return statement
+create function say_something() returns trigger
+language plpgsql as $$
+begin
+    raise notice '%', tg_argv[0];
+end$$;
+-- trigger function with RETURN without argument
+create function say_something2() returns trigger
+language plpgsql as $$
+begin
+    raise notice '%', tg_argv[0];
+    return;
+end$$;
+create trigger say_hello_s before insert on test_triggers for each statement execute procedure say_something('statement trigger');
+create trigger say_hello_ra after update on test_triggers for each row execute procedure say_something('after row trigger');
+create trigger say_hello_ra2 after update on test_triggers for each row execute procedure say_something2('another after row trigger');
+insert into test_triggers values (1, 'foo');
+NOTICE:  statement trigger
+update test_triggers set b = 'bar' where a = 1;
+NOTICE:  after row trigger
+NOTICE:  another after row trigger
+create trigger say_hello_rb before update on test_triggers for each row execute procedure say_something('before row trigger');  -- will fail
+update test_triggers set b = 'baz' where a = 1;
+NOTICE:  before row trigger
+ERROR:  control reached end of trigger procedure without RETURN
+DETAIL:  A trigger procedure used in a row-level trigger that is not fired AFTER must return a value.
+CONTEXT:  PL/pgSQL function "say_something"
+drop trigger say_hello_rb on test_triggers;
+-- XXX plain RETURN behaves like RETURN NULL
+create trigger say_hello_rb2 before update on test_triggers for each row execute procedure say_something2('another before row trigger');
+update test_triggers set b = 'baz' where a = 1;
+NOTICE:  another before row trigger
+select * from test_triggers order by a, b;
+ a |  b  
+---+-----
+ 1 | bar
+(1 row)
+
+drop trigger say_hello_rb2 on test_triggers;
+drop table test_triggers;
+drop function say_something();
+drop function say_something2();
diff --git i/src/test/regress/sql/plpgsql.sql w/src/test/regress/sql/plpgsql.sql
index b47c2de..167b1fe 100644
--- i/src/test/regress/sql/plpgsql.sql
+++ w/src/test/regress/sql/plpgsql.sql
@@ -3600,3 +3600,44 @@ select testoa(1,2,1); -- fail at update
 
 drop function arrayassign1();
 drop function testoa(x1 int, x2 int, x3 int);
+
+--
+-- Additional triggers tests
+--
+
+create table test_triggers (a int, b text);
+
+-- trigger function without return statement
+create function say_something() returns trigger
+language plpgsql as $$
+begin
+    raise notice '%', tg_argv[0];
+end$$;
+
+-- trigger function with RETURN without argument
+create function say_something2() returns trigger
+language plpgsql as $$
+begin
+    raise notice '%', tg_argv[0];
+    return;
+end$$;
+
+create trigger say_hello_s before insert on test_triggers for each statement execute procedure say_something('statement trigger');
+create trigger say_hello_ra after update on test_triggers for each row execute procedure say_something('after row trigger');
+create trigger say_hello_ra2 after update on test_triggers for each row execute procedure say_something2('another after row trigger');
+insert into test_triggers values (1, 'foo');
+update test_triggers set b = 'bar' where a = 1;
+
+create trigger say_hello_rb before update on test_triggers for each row execute procedure say_something('before row trigger');  -- will fail
+update test_triggers set b = 'baz' where a = 1;
+drop trigger say_hello_rb on test_triggers;
+
+-- XXX plain RETURN behaves like RETURN NULL
+create trigger say_hello_rb2 before update on test_triggers for each row execute procedure say_something2('another before row trigger');
+update test_triggers set b = 'baz' where a = 1;
+select * from test_triggers order by a, b;
+drop trigger say_hello_rb2 on test_triggers;
+
+drop table test_triggers;
+drop function say_something();
+drop function say_something2();
#5Pavel Stehule
pavel.stehule@gmail.com
In reply to: Peter Eisentraut (#4)
Re: PL/pgSQL return value in after triggers

2012/1/2 Peter Eisentraut <peter_e@gmx.net>:

On mån, 2011-02-28 at 19:07 +0200, Peter Eisentraut wrote:

PL/pgSQL trigger functions currently require a value to be returned,
even though that value is not used for anything in case of a trigger
fired AFTER.  I was wondering if we could relax that.  It would make
things a bit more robust and produce clearer PL/pgSQL code.  The
specific case I'm concerned about is that a trigger function could
accidentally be run in a BEFORE trigger even though it was not meant for
that.  It is common practice that trigger functions for AFTER triggers
return NULL, which would have unpleasant effects if used in a BEFORE
trigger.

I think it is very uncommon to have the same function usable for BEFORE
and AFTER triggers, so it would be valuable to have coding support
specifically for AFTER triggers.  We could just allow RETURN without
argument, or perhaps no RETURN at all.

Here is a patch for that.

+1

One thing that I'm concerned about with this is that it treats a plain
RETURN in a BEFORE trigger as RETURN NULL, whereas arguably it should be
an error.  I haven't found a good way to handle that yet, but I'll keep
looking.

-1

the change of behave is significant in this case and is better require
some specific symbol. RETURN NULL is good for me.

Regards

Pavel

Show quoted text

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

#6Pavel Stehule
pavel.stehule@gmail.com
In reply to: Peter Eisentraut (#4)
Re: PL/pgSQL return value in after triggers

Hello Peter

I checked code, and I don't think so this is good.

A design of optional NULL is going to inconsistent syntax.

RETURN (OLD, NEW, NULL, /* nothing */) is not consistent

But my main argument is not intuitive behave of BEFORE triggers after
this change.

When somebody write BEFORE trigger function like:

BEGIN
RAISE NOTICE '%', NEW.x;
RETURN;
END;

then don't expect so all rows will be lost.

Preferred default return value for BEFORE INSERT UPDATE trigger should
be NEW, and for DELETE trigger should be OLD - not NULL.

And because we cannot to distinct between BEFORE and AFTER trigger in
parser, I propose don't change current behave. Current behave is not
too friendly - but is consistent with simple rules.

Regards

Pavel

2012/1/2 Peter Eisentraut <peter_e@gmx.net>:

Show quoted text

On mån, 2011-02-28 at 19:07 +0200, Peter Eisentraut wrote:

PL/pgSQL trigger functions currently require a value to be returned,
even though that value is not used for anything in case of a trigger
fired AFTER.  I was wondering if we could relax that.  It would make
things a bit more robust and produce clearer PL/pgSQL code.  The
specific case I'm concerned about is that a trigger function could
accidentally be run in a BEFORE trigger even though it was not meant for
that.  It is common practice that trigger functions for AFTER triggers
return NULL, which would have unpleasant effects if used in a BEFORE
trigger.

I think it is very uncommon to have the same function usable for BEFORE
and AFTER triggers, so it would be valuable to have coding support
specifically for AFTER triggers.  We could just allow RETURN without
argument, or perhaps no RETURN at all.

Here is a patch for that.

One thing that I'm concerned about with this is that it treats a plain
RETURN in a BEFORE trigger as RETURN NULL, whereas arguably it should be
an error.  I haven't found a good way to handle that yet, but I'll keep
looking.

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

#7Robert Haas
robertmhaas@gmail.com
In reply to: Peter Eisentraut (#4)
Re: PL/pgSQL return value in after triggers

On Sun, Jan 1, 2012 at 11:37 PM, Peter Eisentraut <peter_e@gmx.net> wrote:

One thing that I'm concerned about with this is that it treats a plain
RETURN in a BEFORE trigger as RETURN NULL, whereas arguably it should be
an error.  I haven't found a good way to handle that yet, but I'll keep
looking.

I would be very much disinclined to change the behavior of BEFORE
triggers in this way, so I agree we need a way around that.

I'm going to mark this patch Returned with Feedback; I think it's 9.3
material at this point.

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