Can a Postgres Commit Exist in Procedure that has an Exception Block? - postgresql

I am having a difficult time understanding transactions in Postgres. I have a procedure that may encounter an exception. There are parts of the procedure where I might want to commit my work so-far so that it won't be rolled back if an exceptions ensues.
I want to have an exception handling block at the end of the procedure where I catch the exception and insert the information from the exception into a logging table.
I have boiled the problem down to a simple procedure, below, which fails on PostgreSQL 11.2 with
2D000 cannot commit while a subtransaction is active
PL/pgSQL function x_transaction_try() line 6 at COMMIT
drop procedure if exists x_transaction_try;
create or replace procedure x_transaction_try()
language plpgsql
as $$
declare
begin
raise notice 'A';
-- TODO A: do some insert or update that I want to commit no matter what
commit;
raise notice 'B';
-- TODO B: do something else that might raise an exception, without rolling
-- back the work that we did in "TODO A".
exception when others then
declare
my_ex_state text;
my_ex_message text;
my_ex_detail text;
my_ex_hint text;
my_ex_ctx text;
begin
raise notice 'C';
GET STACKED DIAGNOSTICS
my_ex_state = RETURNED_SQLSTATE,
my_ex_message = MESSAGE_TEXT,
my_ex_detail = PG_EXCEPTION_DETAIL,
my_ex_hint = PG_EXCEPTION_HINT,
my_ex_ctx = PG_EXCEPTION_CONTEXT
;
raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
-- TODO C: insert this exception information in a logging table and commit
end;
end;
$$;
call x_transaction_try();
Why doesn't this stored procedure work? Why is it that we never see the output of raise notice 'B' and instead we go into the exception block? Is it possible to do what I have described above with a Postgres 11 stored procedure?
Edit: This is a complete code sample. Paste the above complete code sample (including both the create procedure and call statements) into a sql file and run it in a Postgres 11.2 database to repro. The desired output would be for the function to print A then B, but instead it prints A then C along with the exception information.
Also notice that if you comment out all of the exception handling block such that the function does not catch exceptions at all, then the function will output 'A' then 'B' without an exception occurring. This is why I titled the question the way that I did 'Can a Postgres Commit Exist in Procedure that has an Exception Block?'

The semantics of PL/pgSQL's error handling dictate that:
When an error is caught by an EXCEPTION clause ... all changes to persistent database state within the block are rolled back.
This is implemented using subtransactions, which are basically the same as savepoints. In other words, when you run the following PL/pgSQL code:
BEGIN
PERFORM foo();
EXCEPTION WHEN others THEN
PERFORM handle_error();
END
...what's actually happening is something like this:
BEGIN
SAVEPOINT a;
PERFORM foo();
RELEASE SAVEPOINT a;
EXCEPTION WHEN others THEN
ROLLBACK TO SAVEPOINT a;
PERFORM handle_error();
END
A COMMIT within the block would break this completely; your changes would be made permanent, the savepoint would be discarded, and the exception handler would be left with no way to roll back. As a result, commits are not allowed in this context, and trying to execute a COMMIT will result in a "cannot commit while a subtransaction is active" error.
That's why you see your procedure jump to the exception handler instead of running the raise notice 'B': when it reaches the commit, it throws an error, and the handler catches it.
This is fairly straightforward to work around, though. BEGIN ... END blocks can be nested, and only blocks with EXCEPTION clauses involve setting savepoints, so you can just wrap the commands before and after the commit in their own exception handlers:
create or replace procedure x_transaction_try() language plpgsql
as $$
declare
my_ex_state text;
my_ex_message text;
my_ex_detail text;
my_ex_hint text;
my_ex_ctx text;
begin
begin
raise notice 'A';
exception when others then
raise notice 'C';
GET STACKED DIAGNOSTICS
my_ex_state = RETURNED_SQLSTATE,
my_ex_message = MESSAGE_TEXT,
my_ex_detail = PG_EXCEPTION_DETAIL,
my_ex_hint = PG_EXCEPTION_HINT,
my_ex_ctx = PG_EXCEPTION_CONTEXT
;
raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
end;
commit;
begin
raise notice 'B';
exception when others then
raise notice 'C';
GET STACKED DIAGNOSTICS
my_ex_state = RETURNED_SQLSTATE,
my_ex_message = MESSAGE_TEXT,
my_ex_detail = PG_EXCEPTION_DETAIL,
my_ex_hint = PG_EXCEPTION_HINT,
my_ex_ctx = PG_EXCEPTION_CONTEXT
;
raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
end;
end;
$$;
Unfortunately, it does lead to a lot of duplication in the error handlers, but I can't think of a nice way to avoid it.

The problem is the EXCEPTION clause.
This is implemented in PL/pgSQL as a subtransaction (the same thing as a SAVEPOINT in SQL), which is rolled back when the exception block is reached.
You cannot COMMIT while a subtransaction is active.
See this comment in src/backend/executor/spi.c:
/*
* This restriction is required by PLs implemented on top of SPI. They
* use subtransactions to establish exception blocks that are supposed to
* be rolled back together if there is an error. Terminating the
* top-level transaction in such a block violates that idea. A future PL
* implementation might have different ideas about this, in which case
* this restriction would have to be refined or the check possibly be
* moved out of SPI into the PLs.
*/
if (IsSubTransaction())
ereport(ERROR,
(errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
errmsg("cannot commit while a subtransaction is active")));

Related

Postgres Exceptions

I need to port over some Oracle PL/SQL code to Postgres. This is my first time working with Postgres.
In Oracle, with regards to exceptions, I can have this:
IF v_customer_id IS NULL OR v_email IS NULL THEN
RAISE invalid_paramters;
END IF;
How is that done in Postgres? Basically validating input, and if anything fails validation, call a custom handler to perform whatever actions. From what I have read, Postgres does not support custom named exceptions.
Thanks for your time.
You could use RAISE with a custom message and a specific sqlstate constant:
--Anonymous block for testing purposes
DO $$
BEGIN
RAISE invalid_parameter_value USING MESSAGE = 'Invalid customer or email';
END $$;
Or you could simply raise a generic exception:
DO $$
BEGIN
RAISE EXCEPTION 'A generic exception (P0001)';
END $$;
You could also handle a exception:
DO $$
BEGIN
--This will raise a division by zero
PERFORM 0 / 0;
--You can catch a exception with a EXCEPTION block
EXCEPTION
WHEN division_by_zero THEN
RAISE INFO 'Division by zero catched';
WHEN raise_exception THEN
RAISE INFO 'Another error catched...';
END $$;
And get more detailed information about a exception:
DO $$
DECLARE
error_msg text;
BEGIN
--This will raise a division by zero
PERFORM 0 / 0;
--You can get more about error with GET STACKED DIAGNOSTICS
EXCEPTION
--Tip: OTHERS keyword will catch any exception
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS error_msg = MESSAGE_TEXT;
RAISE EXCEPTION 'My custom exception: %', error_msg;
END $$;
I'd suggest you to take a look on sqlstates and control structures for more about error handling in PostgreSQL.
do $$
declare
v int := 1;
begin
begin
if v < 2 then -- Validation here
raise sqlstate 'A0001'; -- custom SQL state, any five upper case letters and/or digits
end if;
exception -- Catch exception in the nested BEGIN ... END block
when sqlstate 'A0001' then
raise notice 'Here is my exception handler';
v := 2;
end;
raise notice 'Continue here with value %', v; -- Reports that the v = 2
end $$;
Instead of capturing it in the main exception block, you can add a nested begin...end block in the place of RAISE EXCEPTION part and raise an exception inside and capture it in the others exception block inside the nested block with a RETURN keyword.
do
$$
begin
raise notice 'line 1';
begin
raise exception 'raising exception';
exception
when others then
raise notice 'captured in nested block';
end;
return;
raise notice 'check if it continues';
exception
when others then
raise notice 'main block others';
end;
$$
language plpgsql;

PostgreSQL 9.5: Exception handling

I have table called employee with two columns, and have created two functions for
insertion and updation operations. These two function will be called through another
function which named as udf_3().
I want to do exception handling on the third function that is udf_3() which should
give me the details of which function has error.
--Table: employee
create table employee
(
id int,
name varchar(10)
);
--Function 1: udf_1() used for insertion.
create or replace function udf_1()
returns void as
$body$
begin
insert into employee values(1,'Mak');
end;
$body$
language plpgsql;
--Function 2: udf_2() used for updation.
create or replace function udf_2()
returns void as
$body$
begin
update employee
set a_id = 99
where name = 'Mak';
end;
$body$
language plpgsql;
--Function 3: udf_3() used to call all above function.
create or replace function udf_3()
returns int as
$body$
begin
perform udf_1();
perform udf_2();
return 0;
exception
when others then
RAISE INFO 'Error Name:%',SQLERRM;
RAISE INFO 'Error State:%', SQLSTATE;
return -1;
end;
$body$
language plpgsql;
--Function Calling:
select * from udf_3();
Exception:
INFO: Error Name:column "a_id" of relation "employee" does not exist
INFO: Error State:42703
Problem: I am able to get the exception BUT not able to get from which function i got exception.
According to the documentation
Within an exception handler, one may also retrieve information about the current exception by using the GET STACKED DIAGNOSTICS command
https://www.postgresql.org/docs/9.5/static/plpgsql-control-structures.html#PLPGSQL-EXCEPTION-DIAGNOSTICS
Example:
create or replace function udf_3()
returns int as
$body$
declare
err_context text;
begin
perform udf_1();
perform udf_2();
return 0;
exception
when others then
GET STACKED DIAGNOSTICS err_context = PG_EXCEPTION_CONTEXT;
RAISE INFO 'Error Name:%',SQLERRM;
RAISE INFO 'Error State:%', SQLSTATE;
RAISE INFO 'Error Context:%', err_context;
return -1;
end;
$body$
language plpgsql;
will display the following:
INFO: Error Context:SQL: "SELECT udf_1()"
But this is just a textual representation of the error. Your logic should not rely on it. It is better to use custom error codes to handle exception logic (and raise meaningful exceptions in your functions that you can catch and handle later on).
UPDATE:
Another solution is to separate your code in different blocks for which you can catch exceptions separately. In this case you know from which block the exception was raised:
DO $$
BEGIN
-- Block 1
BEGIN
-- any code that might raise an exception
RAISE EXCEPTION 'Exception 1'; -- for example
EXCEPTION
WHEN others THEN
RAISE INFO 'Caught in Block 1';
END;
-- Block 2
BEGIN
-- any code that might raise an exception
RAISE EXCEPTION 'Exception 2'; -- for example
EXCEPTION
WHEN others THEN
RAISE INFO 'Caught in Block 2';
END;
END $$

How does one report the line on which an error occured in Postgres/plpgsql?

I have been using something more or less like this in postgres to emulate how, in SQL Server, I have used Try/Catch blocks with transactions that can be rolled back in the Catch if an error is found:
do $$
begin
[SQL here]
exception when others then
raise notice 'Error in insert statement ---> % %', SQLERRM, SQLSTATE LINE;
end;
$$ language 'plpgsql';
Is there a way to report the line where the error occurred, like "ERROR_LINE()?
Thanks in advance
On some modern PostgreSQL, you can use GET STACKED DIAGNOSTICS statement. There is not possibility to get linenumber, but you can get a call context, where lineno is included:
postgres=> DO $$
DECLARE
a int DEFAULT 0;
_c text;
BEGIN
BEGIN
PERFORM 10/a;
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS _c = PG_EXCEPTION_CONTEXT;
RAISE NOTICE 'context: >>%<<', _c;
END;
END;
$$;
NOTICE: 00000: context: >>SQL statement "SELECT 10/a"
PL/pgSQL function inline_code_block line 7 at PERFORM<<
LOCATION: exec_stmt_raise, pl_exec.c:3041
DO
DO $$
DECLARE
a int DEFAULT 0;
m_sqlstate text;
m_message text;
m_context text;
m_PG_EXCEPTION_HINT text;
BEGIN
PERFORM 10/a;
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS
m_sqlstate = returned_sqlstate,
m_message = message_text,
m_context = pg_exception_context,
m_PG_EXCEPTION_HINT = PG_EXCEPTION_HINT;
RAISE NOTICE 'sqlstate: %', m_sqlstate; --show for error code .
RAISE NOTICE 'message: %', m_message;
RAISE NOTICE 'context: %', m_context;
RAISE NOTICE 'The exception detail: %', m_PG_EXCEPTION_HINT;
END
$$;
Result:
NOTICE: sqlstate: 22012
NOTICE: message: division by zero
NOTICE: context: SQL statement "SELECT 10/a"
PL/pgSQL function inline_code_block line 9 at PERFORM
NOTICE: The exception detail:
DO
Manual reference for get diagnostic stack. Especially check
Table 43.2. Error Diagnostics Items
PostgreSQL Error Codes In manual appendix, that is not easy to
find.
As you can see that: pg_exception_context will report the line occurred.
tested on PostgreSQL version: 14

How to return an integer from a plpgsql function AND rollback every modifications?

My application uses pl/pgsql functions that return integers.
The returned integer is used as a return code, to distinguish between different errors.
For example, if a function that insert datas returns -1, it means that some datas exist already and this is forbidden to try to insert the same data again. But if it returns -2, it means something else. That way the application knows the error and can display a useful error message to the user.
My problem now is that i want, at some points in the function, to return immediately when i detect an error, and rollback everything done so far in the function. If i use "raise exception", it will rollback, but not return an integer. If i use "return -1;", it will return an integer, but not rollback modifications. So i'm stuck, because obviously i can't do both
Here's a phony example function:
CREATE OR REPLACE FUNCTION create_flight_and_passengers(
_date timetamp,
_name text,
_passengers integer[]
)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
return_code integer;
BEGIN
INSERT INTO flights
VALUES (_name);
SELECT function_1(_date, _passengers) into return_code;
if (return_code = -1) then
-- [1] rollback everything done since beginning of function
-- create_flight_and_passengers() and return -1
end if;
SELECT function_2(_date, _passengers) into return_code;
if (return_code = -1) then
-- [2] rollback everything done since beginning of function
-- create_flight_and_passengers() and return -2
end if;
return 0;
END;
$$;
In [1] and [2] i could use raise exception to rollback, but when i do this i don't have a returned integer.
I could set a local variable in [1] and [2], then raise exception, and in EXCEPTION do tests on this variable to know where the exception come from, but this is bloated, there must be something better !
I'm not even sure you can rollback effects of a function you have called and that has terminated (function_1() and function_2() in my example)
Any ideas ?
This is a pretty bizarre thing to want to do. If you really need to, you could do it like this:
DECLARE
retval integer;
BEGIN
retval := 0;
BEGIN
... make my changes ...
IF (... is something wrong? ...) THEN
RAISE EXCEPTION SQLSTATE '0U001';
END IF;
EXCEPTION
WHEN '0U001' THEN
retval := -1;
END;
END;
The concept here is that a BEGIN ... EXCEPTION block defines a subtransaction. A RAISE EXCEPTION within the block rolls back the subtransaction. We catch it at the outer level, preventing the exception from propagating outside the function and aborting the whole transaction.
See the PL/PgSQL documentation.

Raising error in postgreSQL

CREATE OR REPLACE FUNCTION msgfailerror() RETURNS trigger AS
' BEGIN
IF NEW.noces< new.first_column THEN
RAISE EXCEPTION 'cannot have a negative salary';
END IF;
return new;
END' LANGUAGE plpgsql
Trigger
create trigger msgfail before insert on first for each row
execute procedure msgfailerror()
Giving error:
syntax error at or near "cannot" LINE 5: RAISE
EXCEPTION 'cannot have a negative ...
I have almost one validation for each field of row. I want trigger to check all validations while insertion is being done and, raise error log afterwards once for all. Should I use raise exception on raise notice ?
For example:
Insert into first (first_column, noces,dob) values ('4545','75','545')
I am checking noces is less than first_column, for the same row i want to check if dob > 80 and if first_column is integer and raise error for all validations. Thanks in advance
The quoting is wrong. It's easier to use dollar quotes $$:
CREATE OR REPLACE FUNCTION msgfailerror()
RETURNS trigger AS
$$
BEGIN
IF NEW.noces< new.first_column THEN
RAISE EXCEPTION 'cannot have a negative salary';
END IF;
return new;
END;
$$
LANGUAGE plpgsql;
But on the other hand, what's wrong with a check constraint?
there is nothing wrong with you the only thing is using quotes
please change :
RAISE EXCEPTION 'cannot have a negative salary';
to:
RAISE EXCEPTION ''cannot have a negative salary'';
'' is different from "
'' = two single quotes
I agree with Frank that you could better use constraints, but you call it validation. Validation is typically done before insertion takes place. If you would like to validate insertions, you could use functions instead of triggers or constraints.
When you would write functions is the answer to your question to raise exceptions or notices that as long as there has been no write action a notice would suffice (together with leaving the function). Once there has been a write to the database, do you have to use exceptions as they perform a rollback.
Like this:
CREATE OR REPLACE FUNCTION field_validate(p_int int) RETURNS boolean AS $$
DECLARE
i_id int;
BEGIN
if p_int > 10 then
raise notice 'should be smaller then 10';
return false;
end if;
insert into tbl_numbers(firstfield) values(p_int) returning id in i_id;
insert into tbl_fake(nofield) values(i_id);
return true;
EXCEPTION
WHEN raise exception THEN
return false;
END;
$$ LANGUAGE plpgsql;