Rollback Transaction on Trigger ERROR - sql

I'm trying to check if the room that is going to be inserted in the system is already rented at that date or not. I've though about counting the rows that match both the room number and the date, and then rolling back the transaction. But I'm getting the following error, even though I have changed the code to raise user-defined exceptions:
ERROR: cannot begin/end transactions in PL/pgSQL
HINT: Use a BEGIN block with an EXCEPTION clause instead.
CONTEXT: PL/pgSQL function "checkRoom"() line 17 at SQL statement
CREATE OR REPLACE FUNCTION "checkRoom"() RETURNS TRIGGER AS
$BODY$
DECLARE
counter integer;
BEGIN
SELECT COUNT("num_sesion")
FROM "Sesion"
INTO counter
WHERE "Room_Name"=NEW."Room_Name" AND "Date"=NEW."Date";
IF (counter> 0) THEN -- Probably counter>1 as it's triggered after the transaction..
raise notice 'THERE'S A ROOM ALREADY!!';
raise exception 'The room is rented at that date';
END IF;
RETURN new;
EXCEPTION
WHEN raise_exception THEN
ROLLBACK TRANSACTION;
RETURN new;
END;$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF;
Then I create the trigger:
CREATE TRIGGER "roomOcupied" AFTER INSERT OR UPDATE OF "Room_Name", "Date"
ON "Sesion" FOR EACH ROW
EXECUTE PROCEDURE "checkRoom"();
It's been 2 years from my last approach to SQL and the changes between plsql and plpgsql are getting me crazy.

A couple of issues with your trigger function:
Use IF EXISTS (...) THEN instead of counting all occurrences. Faster, simpler. See:
PL/pgSQL checking if a row exists
A trigger function AFTER INSERT OR UPDATE can just return NULL. RETURN NEW is only relevant for triggers called BEFORE. The manual:
The return value is ignored for row-level triggers fired after an operation, and so they can return NULL.
Unbalanced single quote.
As #Pavel explained, you cannot control transactions from within a plpgsql function. Any unhandled exception forces your entire transaction to be rolled back automatically. So, just remove the EXCEPTION block.
Your hypothetical trigger rewritten:
CREATE OR REPLACE FUNCTION check_room()
RETURNS TRIGGER AS
$func$
BEGIN
IF EXISTS (
SELECT FROM "Sesion" -- are you sure it's not "Session"?
WHERE "Room_Name" = NEW."Room_Name"
AND "Date" = NEW."Date") THEN
RAISE EXCEPTION 'The room is rented at that date';
END IF;
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
A BEFORE trigger makes more sense.
But a UNIQUE INDEX ON ("Room_Name", "Date") would do the same, more efficiently. Then, any row in violation raises a duplicate key exception and rolls back the transaction (unless caught and handled). In modern Postgres you can alternatively skip or divert such INSERT attempts with INSERT ... ON CONFLICT .... See:
How to UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) in PostgreSQL?
Advanced usage:
How to use RETURNING with ON CONFLICT in PostgreSQL?

PostgreSQL processes errors significantly differently from other databases. Any unhandled errors are raised to the user. Inside PL/pgSQL you can trap any exception or you can raise any exception, but you cannot explicitly control transactions. Any PostgreSQL statement is executed inside of a transaction (functions too). And the most outer transaction is automatically broken when any unhandled exception goes to the top.
What you can:
raise exception (often in triggers)
BEGIN
IF CURRENT_USER <> 'Admin' THEN
RAISE EXCEPTION 'missing admin rights';
END IF;
RETURN NEW;
END;
trapping exception
BEGIN
BEGIN -- start of protected section
-- do some, what can be stopped by exception
EXCEPTION WHEN divide_by_zero THEN
-- exception handler;
RAISE WARNING 'I was here';
-- should ignore
EXCEPTION WHEN others THEN
-- any unexpected exception
RAISE WARNING 'some unexpected issue';
RAISE; -- forward exception'
END;
There is no other possibility - so writing application in PL/pgSQL is very simple, but different than PL/SQL or TSQL.

Related

Rollback in procedure called over dblink

I have strange situation
It's bit hard to explain but I'll do my best
There are 3 different database included
From DB1 I call function on DB2 (over dblink)
That procedure calls another procedure that inserts data into table on DB3
Function on DB2 has EXCEPTION handle that should rollback everything that it did in case of exception
I did example run, and everything went well (there was no error) but insert from procedure 3 was not rollbacked and I have to rollback from DB1 to truly rollback
If i commit from db1, row is inserted
Am I doing something wrong and is there a way to rollback directly from function on db2
Here is some example code:
--DB1
PROCEDURE 1
BEGIN
x := function2#dblink_to_db2();
END;
--DB2
FUNCTION 2
BEGIN
procedure3();
RAISE SOME EXCEPTION;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
do_something_else();
RETURN 0;
END;
PROCEDURE 3
BEGIN
INSERT INTO tableA#dblink_to_db3 VALUES ... ;
END;
So no error is raised but insert into table on db3 is not rollbacked
You must be having a commit somewhere in your code either before the raise exception or in Procedure3. I just tested the below code and it rolledback everything before the exception. Please ignore the naming conventions, had to go due to time constraint.
Database 3
CREATE TABLE temp
(col1 NUMBER);
create or replace procedure testp(i number)
as
BEGIN
INSERT INTO temp VALUES (i);
END;
/
Database2
CREATE OR REPLACE FUNCTION DLR_TRANS.testf(i number)
return number
as
e exception;
BEGIN
testp#TO_DB3(i);
RAISE e;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RETURN 0;
END;
/
database1
declare
x number;
BEGIN
x := testf#TO_DB2(15);
DBMS_OUTPUT.PUT_LINE ( 'x = ' || x );
commit;
END;
x returns 0 due to exception in DB2's function.
And below is the data when I ran the below statement on DB3
select * from temp;
Hope this helps
The problem is that you have "handled" the exception in [function 2]. You should not put the exception block in [function 2] at all. And let the exception propagate up to [procedure 1]. Here you will implicitly or explicitly rollback.
If you must have an exception block in [function 2] then you should have a [raise] at the end. Handling the exception like this is saying [I have handled it and for all practical purposes the caller of this should not think anythibg bad has happened]

How to save table progress in postgresql between transaction?

Tables are updating in loop, but if error come in one of table than transaction failed and all the tables data updated is gone so provide me the solution in which each time any table is update that its progress can save.
d0
$$
declare g record;
declare tablename varchar(50);
BEGIN
--fetching tablename from catalog.table
for g in execute formate ('select table_name from catalog.table');
loop
tablename= lower(g.tablename);
--passing tablename to function for some execution
execute'select function('''||tablename||''')';
end loop;
end;
$$
The transaction won't fail if you trap the error.
BEGIN
execute your query
EXCEPTION WHEN unique_violation OR foreign_key_violation OR ... THEN
END;
When a function or codeblock is executed there is always already a transaction either created explicitly with a BEGIN or automatically. The BEGIN of the exception block acts as a SAVEPOINT in the transaction. When the error is trapped by the EXCEPTION part only the work after the BEGIN is lost because it rollsback to the savepoint.
When you let an error escape from the function a rollback of the whole transaction is done.
For details see the manual.
BTW. postgresql 9.1 is not being maintained you should consider upgrading.

How to get the row count of update,insert,delete query in postgresql

I have a postgresql tables. user may update records or delete records manually using interface or using query. I need to track how many records have been updated ,deleted into a log table.I had tried as below update operations count can be tracked successfully. But delete operations count are not logging to log table.
CREATE OR REPLACE FUNCTION edmonton.count_func() RETURNS TRIGGER AS $body$
DECLARE
v_old_data TEXT;
v_new_data TEXT;
log_count INTEGER;
BEGIN
IF (TG_OP = 'UPDATE') THEN
v_old_data := ROW(OLD.*);
v_new_data := ROW(NEW.*);
log_count = count(row(new));
RAISE NOTICE 'update operations';
INSERT INTO edmonton.log ("timestamp",message,type,project_area,count)
VALUES (CURRENT_TIMESTAMP,'records updated in Builder
table','Information','edmonton',log_count);
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
v_old_data := ROW(OLD.*);
log_count = count(row(old));
RAISE NOTICE 'delete operations';
INSERT INTO edmonton.log ("timestamp",message,type,project_area,count)
VALUES (CURRENT_TIMESTAMP,'records deleted in Builder
table','Information','edmonton',log_count);
RETURN OLD;
ELSIF (TG_OP = 'INSERT') THEN
v_new_data := ROW(NEW.*);
log_count = count(row(new));
RAISE NOTICE 'insert operations';
INSERT INTO edmonton.log ("timestamp",message,type,project_area,count)
VALUES (CURRENT_TIMESTAMP,'records inserted in Builder
table','Information','edmonton',log_count);
RETURN NEW;
ELSE
RAISE WARNING '[EDMONTON.COUNT_FUNC] - Other action occurred: %, at
%',TG_OP,now();
RETURN NULL;
END IF;
EXCEPTION
WHEN data_exception THEN
RAISE WARNING '[EDMONTON.COUNT_FUNC] - UDF ERROR [DATA EXCEPTION] -
SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
RETURN NULL;
WHEN unique_violation THEN
RAISE WARNING '[EDMONTON.COUNT_FUNC] - UDF ERROR [UNIQUE] - SQLSTATE: %,
SQLERRM: %',SQLSTATE,SQLERRM;
RETURN NULL;
WHEN OTHERS THEN
RAISE WARNING '[EDMONTON.COUNT_FUNC] - UDF ERROR [OTHER] - SQLSTATE: %,
SQLERRM: %',SQLSTATE,SQLERRM;
RETURN NULL;
END;
$body$
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, edmonton;
And this is my trigger
CREATE TRIGGER count_audit
AFTER INSERT OR UPDATE OR DELETE
ON edmonton.builder
FOR EACH STATEMENT
EXECUTE PROCEDURE edmonton.count_func();
I am getting a warning message when updating a record and messages were not logged to log table.
WARNING: [EDMONTON.COUNT_FUNC] - UDF ERROR [OTHER] - SQLSTATE: 55000, SQLERRM: record "old" is not assigned yet
CONTEXT: PL/pgSQL function count_func() line 49 at RAISE
First, you are not able to count inserted/updated/deleted rows that way. The expression
count(row(new))
always returns 1 as row(new) is a single row.
Second, the records new and old are not set in a trigger for each statement. Per the documentation:
NEW
Data type RECORD; variable holding the new database row for INSERT/UPDATE operations in row-level triggers. This variable is unassigned in statement-level triggers and for DELETE operations.
https://www.postgresql.org/docs/current/static/plpgsql-trigger.html
NEW
Data type RECORD; variable holding the new database row for
INSERT/UPDATE operations in row-level triggers. This variable is
unassigned in statement-level triggers and for DELETE operations.
OLD
Data type RECORD; variable holding the old database row for
UPDATE/DELETE operations in row-level triggers. This variable is
unassigned in statement-level triggers and for INSERT operations.
So the real question is
how does update/insert work?..

NO_DATA_FOUND exception not thrown when used in SELECT INTO

I noticed strange behaviour of NO_DATA_FOUND exception when thrown from function used in PLSQL.
Long story short - it does propagate from function when using assignment, and does not propagate (or is handled silently somewhere in between) when used in SELECT INTO.
So, given function test_me throwing NO_DATA_FOUND exception, when invoked as:
v_x := test_me(p_pk);
It throws an exception, while when invoked as:
SELECT test_me(p_pk) INTO v_x FROM dual;
it does not throw exception. This does not occur with other exceptions. Below You can find my test examples.
Could somebody please explain to me this behaviour?
set serveroutput on;
CREATE OR REPLACE FUNCTION test_me(p_pk NUMBER) RETURN NVARCHAR2
IS
v_ret NVARCHAR2(50 CHAR);
BEGIN
BEGIN
SELECT 'PYK' INTO v_ret FROM dual WHERE 1 = 1/p_pk;
EXCEPTION WHEN NO_DATA_FOUND THEN
dbms_output.put_line(chr(9)||chr(9)||chr(9)||' (test_me NO_DATA_FOUND handled and rerised)');
RAISE;
END;
RETURN v_ret;
END;
/
DECLARE
v_x NVARCHAR2(500 CHAR);
v_pk NUMBER;
PROCEDURE test_example(p_pk NUMBER)
IS
BEGIN
BEGIN
dbms_output.put_line(chr(9)||chr(9)||'Test case 1: Select into.');
SELECT test_me(p_pk) INTO v_x FROM dual;
dbms_output.put_line(chr(9)||chr(9)||'Success: '||NVL(v_x,'NULL RETURNED'));
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line(chr(9)||chr(9)||'Failure: NO_DATA_FOUND detected');
WHEN OTHERS THEN
dbms_output.put_line(chr(9)||chr(9)||'Failure: '||SQLCODE||' detected');
END;
dbms_output.put_line(' ');
BEGIN
dbms_output.put_line(chr(9)||chr(9)||'Test case 2: Assignment.');
v_x := test_me(p_pk);
dbms_output.put_line(chr(9)||chr(9)||'Success: '||NVL(v_x,'NULL RETURNED'));
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line(chr(9)||chr(9)||'Failure: NO_DATA_FOUND detected');
WHEN OTHERS THEN
dbms_output.put_line(chr(9)||chr(9)||'Failure: '||SQLCODE||' detected');
END;
END;
BEGIN
dbms_output.put_line('START');
dbms_output.put_line(' ');
dbms_output.put_line(chr(9)||'Example 1: Function throws some exception, both cases throws exception, everything is working as expected.');
test_example(0);
dbms_output.put_line(' ');
dbms_output.put_line(chr(9)||'Example 2: Query returns row, there is no exceptions, everything is working as expected.');
test_example(1);
dbms_output.put_line(' ');
dbms_output.put_line(chr(9)||'Example 3: Query inside function throws NO_DATA_FOUND, strange things happen - one case is throwing exception, the other is not.');
test_example(2);
dbms_output.put_line(' ');
dbms_output.put_line('END');
END;
/
DROP FUNCTION test_me;
A minimal example is:
CREATE FUNCTION raise_exception RETURN INT
IS
BEGIN
RAISE NO_DATA_FOUND;
END;
/
If you do:
SELECT raise_exception
FROM DUAL;
You will get a single row containing a NULL value - Ask Tom states:
it has ALWAYS been that way
and then followed up with:
no_data_found is not an error - it is an "exceptional condition". You, the programmer, decide if something is an error by catching the exceptional condition and handling it (making it be "not an error") or ignoring it (making it be an error).
in sql, no data found quite simply means "no data found", stop.
Under the covers, SQL is raising back to the client application "hey buddy -- no_data_found". The
client in this case says "ah hah, no data found means 'end of data'" and stops.
So the exception is raised in the function and the SQL client sees this and interprets this as there is no data which is a NULL value and "handles" the exception.
So
DECLARE
variable_name VARCHAR2(50);
BEGIN
SELECT raise_exception
INTO variable_name
FROM DUAL
END;
/
Will succeed as the DUAL table has a single row and the exception from the function will be handled (silently) and the variable will end up containing a NULL value.
However,
BEGIN
DBMS_OUTPUT.PUT_LINE( raise_exception );
END;
/
The exception is this time being passed from the function to a PL/SQL scope - which does not handle the error and passes the exception to the exception handler block (which does not exist) so then gets passed up to the application scope and terminates execution of the program.
And Ask Tom states:
Under the covers, PLSQL is raising back to the client application "hey -- no_data_found. The client in this case says "uh-oh, wasn't expecting that from PLSQL -- sql sure, but not PLSQL. Lets print out the text that goes with this exceptional condition and continue on"
You see -- it is all in the way the CLIENT interprets the ORA-xxxxx message. That message, when raised by SQL, is interpreted by the client as "you are done". That message, when raised by PLSQL and not handled by the PLSQL programmer, is on the other hand interpreted as "a bad thing just happened"
Both PLSQL and SQL actually do the same thing here. It is the CLIENT that is deciding to do something different.
Now, if we change the function to raise a different exception:
CREATE OR REPLACE FUNCTION raise_exception RETURN INT
IS
BEGIN
RAISE ZERO_DIVIDE;
END;
/
Then both:
SELECT raise_exception
FROM DUAL;
and:
BEGIN
DBMS_OUTPUT.PUT_LINE( raise_exception );
END;
/
do not know how to handle the exception and terminate with ORA-01476 divisor is equal to zero.

Why is the exception NO_DATA_FOUND not being triggered?

So the problem i am having is that if i execute the following procedure and the cursor doesnt find the parameter being passed, it continues to execute the block (insert statement) but instead of throwing the NO_DATA_FOUND exception error it throws a parent/foreign key error.
CREATE OR REPLACE PACKAGE ASSIGNMENT3 IS
PROCEDURE END_CAMPAIGN(CTITLE IN CAMPAIGN.CAMPAIGNTITLE%TYPE);
END ASSIGNMENT3;
/
CREATE OR REPLACE PACKAGE BODY ASSIGNMENT3 AS
PROCEDURE END_CAMPAIGN(CTITLE IN CAMPAIGN.CAMPAIGNTITLE%TYPE) IS
CURSOR ADCOST_CUR IS
SELECT ACTUALCOST
FROM ADVERTISEMENT
WHERE ADVERTISEMENT.CAMPAIGNTITLE = CTITLE;
V_TOTALCOST NUMBER;
BEGIN
V_TOTALCOST := 0;
FOR INVOICE_REC IN ADCOST_CUR
LOOP
V_TOTALCOST := V_TOTALCOST + INVOICE_REC.ACTUALCOST;
END LOOP;
INSERT INTO INVOICE(INVOICENO, CAMPAIGNTITLE, DATEISSUED, DATEPAID, BALANCEOWING, STATUS)
VALUES (AUTOINCREMENTINVOICE.nextval, CTITLE, SYSDATE, NULL,V_TOTALCOST,NULL);
EXCEPTION WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('ERROR:The campaign title you entered returned no record(s), please enter a valid campaign title.');
COMMIT;
END END_CAMPAIGN;
END ASSIGNMENT3;
/
SET SERVEROUTPUT ON
EXECUTE ASSIGNMENT3.END_CAMPAIGN('Panasonic 3D TV');
While the parent foreign key error is correct, i dont want the block to execeute if the cursor doesnt return a row. Why is this happening?
Also, in terms of placing the COMMIT, where exactly do i tell it to COMMIT? Before the exception or after?
This is for a uni assignment.
When you loop over a cursor like that, if the cursor finds no matching rows, the loop simply doesn't execute at all. A NO_DATA_FOUND exception would only be raised if you had a SELECT ... INTO ... statement inside the BEGIN/END block that did not return any rows.
Where you have the COMMIT placed now, it is part of the EXCEPTION block -- but your indentation implies that you want it to execute whether the exception occurred or not. In this case, I would just put the COMMIT immediately after the INSERT, since it only matters if the INSERT is successful.
"So is there no way to have the NODATAFOUND exception trigger when
using a cursor, if the CTITLE parameter isnt found in the table"
What you could do is test the value of V_TOTAL_COST. If it is zero raise an exception, like this:
PROCEDURE END_CAMPAIGN(CTITLE IN CAMPAIGN.CAMPAIGNTITLE%TYPE) IS
CURSOR ADCOST_CUR IS
SELECT ACTUALCOST
FROM ADVERTISEMENT
WHERE ADVERTISEMENT.CAMPAIGNTITLE = CTITLE;
V_TOTALCOST NUMBER;
BEGIN
V_TOTALCOST := 0;
FOR INVOICE_REC IN ADCOST_CUR
LOOP
V_TOTALCOST := V_TOTALCOST + INVOICE_REC.ACTUALCOST;
END LOOP;
if v_total_cost = 0 then
raise no_data_found;
end if;
INSERT INTO INVOICE(INVOICENO, CAMPAIGNTITLE, DATEISSUED, DATEPAID, BALANCEOWING, STATUS)
VALUES (AUTOINCREMENTINVOICE.nextval, CTITLE, SYSDATE, NULL,V_TOTALCOST,NULL);
COMMIT;
EXCEPTION WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('ERROR:The campaign title you entered returned no record(s), please enter a valid campaign title.');
END END_CAMPAIGN;
This assumes you have a business rule that ACTUAL_COST cannot be zero.
Alternatively, there is the clunkier workaround of incrementing a counter in the loop and testing whether it is zero after the loop.
As for where to place the commit I would say the answer is not inside the procedure. The client (sqlplus in this case) should determine if the transaction will commit or rollback as the call to end the campaign may be just a part of a wider process. Also assuming that a campaign can exist without any advertisements then I would have an explicit check that the campaign title is valid perhaps against the table of CAMPAIGN? as suggested below:
CREATE OR REPLACE PACKAGE ASSIGNMENT3 IS
PROCEDURE END_CAMPAIGN(CTITLE IN CAMPAIGN.CAMPAIGNTITLE%TYPE);
END ASSIGNMENT3;
/
CREATE OR REPLACE PACKAGE BODY ASSIGNMENT3 AS
PROCEDURE END_CAMPAIGN(CTITLE IN CAMPAIGN.CAMPAIGNTITLE%TYPE) IS
V_VALID_CAMPAIGN INTEGER;
V_TOTALCOST NUMBER;
BEGIN
-- Check this campaign title is valid
/* Will get you NO_DATA_FOUND here if CTITLE is invalid so wrap in
another BEGIN END block to throw own custom error that the client
of this procedure can handle (if it wants) */
BEGIN
SELECT 1
INTO V_VALID_CAMPAIGN
FROM CAMPAIGN
WHERE CAMPAIGNTITLE = CTITLE;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20000,'The campaign title you entered returned no record(s), please enter a valid campaign title.');
END;
-- Now tot up the cost of ads in this campaign and raise the invoice
SELECT SUM(ACTUALCOST)
INTO V_TOTALCOST
FROM ADVERTISEMENT
WHERE ADVERTISEMENT.CAMPAIGNTITLE = CTITLE;
INSERT INTO INVOICE(INVOICENO, CAMPAIGNTITLE, DATEISSUED, DATEPAID, BALANCEOWING, STATUS)
VALUES (AUTOINCREMENTINVOICE.nextval, CTITLE, SYSDATE, NULL,V_TOTALCOST,NULL);
END END_CAMPAIGN;
END ASSIGNMENT3;
/
EXECUTE ASSIGNMENT3.END_CAMPAIGN('Panasonic 3D TV');
COMMIT;