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.
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();
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers