I have written a stored procedure that basically loops over an array of fields and performs some manipulation in the db for each iteration. What I want to achieve is, either all the iterations of loops should occur or neither one of them should occur.
So let's say there were 5 elements in the fields array and the loop iterates up to the 3rd element before noticing that some condition is true and throwing an error, I want to rollback all the changes that occurred during the first 2 iterations. I've used ROLLBACK statements to achieve the same, but every time it reaches the ROLLBACK statement it throws the following error:
Cannot rollback while a subtransaction is active : 2D000
Surprisingly, it works as normal if I comment out the outobj := json_build_object('code',0); statement within the EXCEPTION WHEN OTHERS THEN block or if I remove that whole block completely.
I've checked the PostgreSQL documentation for error codes, but it didn't really help. My stored procedure is as follows:
CREATE OR REPLACE PROCEDURE public.usp_add_fields(
field_data json,
INOUT outobj json DEFAULT NULL::json)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
v_user_id bigint;
farm_and_bussiness json;
_field_obj json;
_are_wells_inserted boolean;
BEGIN
-- get user id
v_user_id = ___uf_get_user_id(json_extract_path_text(field_data,'user_email'));
IF(v_user_id IS NULL) THEN
outobj := json_build_object('code',17);
RETURN;
END IF;
-- Loop over entities to create farms & businesses
FOR _field_obj IN SELECT * FROM json_array_elements(json_extract_path(field_data,'fields'))
LOOP
-- check if irrigation unit id is already linked to some other field
IF(SELECT EXISTS(
SELECT field_id FROM user_fields WHERE irrig_unit_id LIKE json_extract_path_text(_field_obj,'irrig_unit_id') AND deleted=FALSE
)) THEN
outobj := json_build_object('code',26);
-- Rollback any changes made by previous iterations of loop
ROLLBACK;
RETURN;
END IF;
-- check if this field name already exists
IF( SELECT EXISTS(
SELECT uf.field_id FROM user_fields uf
INNER JOIN user_farms ufa ON (ufa.farm_id=uf.user_farm_id AND ufa.deleted=FALSE)
INNER JOIN user_businesses ub ON (ub.business_id=ufa.user_business_id AND ub.deleted=FALSE)
INNER JOIN users u ON (ub.user_id = u.user_id AND u.deleted=FALSE)
WHERE u.user_id = v_user_id
AND uf.field_name LIKE json_extract_path_text(_field_obj,'field_name')
AND uf.deleted=FALSE
)) THEN
outobj := json_build_object('code', 22);
-- Rollback any changes made by previous iterations of loop
ROLLBACK;
RETURN;
END IF;
--create/update user business and farm and return farm_id
CALL usp_add_user_bussiness_and_farm(
json_build_object('user_email', json_extract_path_text(field_data,'user_email'),
'business_name', json_extract_path_text(_field_obj,'business_name'),
'farm_name', json_extract_path_text(_field_obj,'farm_name')
), farm_and_bussiness);
IF(json_extract_path_text(farm_and_bussiness, 'code')::int != 1) THEN
outobj := farm_and_bussiness;
-- Rollback any changes made by previous iterations of loop
ROLLBACK;
RETURN;
END IF;
-- insert into users fields
INSERT INTO user_fields (user_farm_id, irrig_unit_id, field_name, ground_water_percent, surface_water_percent)
SELECT json_extract_path_text(farm_and_bussiness,'farm_id')::bigint,
json_extract_path_text(_field_obj,'irrig_unit_id'),
json_extract_path_text(_field_obj,'field_name'),
json_extract_path_text(_field_obj,'groundWaterPercentage'):: int,
json_extract_path_text(_field_obj,'surfaceWaterPercentage'):: int;
-- add to user wells
CALL usp_insert_user_wells(json_extract_path(_field_obj,'well_data'), v_user_id, _are_wells_inserted);
END LOOP;
outobj := json_build_object('code',1);
RETURN;
EXCEPTION WHEN OTHERS THEN
raise notice '% : %', SQLERRM, SQLSTATE;
outobj := json_build_object('code',0);
RETURN;
END;
$BODY$;
If you have an EXCEPTION clause in a PL/pgSQL block, that whole block will be executed in a subtransaction that is rolled back when an exception happens. So you cannot use COMMIT or ROLLBACK in such a block.
If you really need that ROLLBACK, rewrite your code like this:
DECLARE
should_rollback boolean := FALSE;
BEGIN
FOR ... LOOP
BEGIN -- inner block for exception handling
/* do stuff */
IF (/* condition that should cause a rollback */) THEN
should_rollback := TRUE;
EXIT; -- from LOOP
END IF;
EXCEPTION
WHEN OTHERS THEN
/* handle the error */
END;
END LOOP;
IF should_rollback THEN
ROLLBACK;
/* do whatever else is needed */
END IF;
END;
Now the rollback does not happen in a block with an exception handler, and it should work the way you want.
Explanation:
Based on the clue provided by #Laurez Albe, I came up with a cleaner way to solve the above problem.
Basically, what I've done is, I've raised a custom exception whenever a condition is true. So when an exception is thrown, all the changes made by block X are rolled back gracefully. I can even perform last minute cleanup within the exception conditional blocks.
Implementation:
CREATE OR REPLACE procedure mProcedure(INOUT resp json DEFAULT NULL::JSON)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
field_data json := '{ "fields": [1,2,3,4,5] }';
_field_id int;
BEGIN
-- Start of block X
FOR _field_id IN SELECT * FROM json_array_elements(json_extract_path(field_data,'fields'))
LOOP
INSERT INTO demo VALUES(_field_id);
IF(_field_id = 3) THEN
RAISE EXCEPTION USING ERRCODE='22013';
END IF;
IF(_field_id = 5) THEN
RAISE EXCEPTION USING ERRCODE='22014';
END IF;
END LOOP;
SELECT json_agg(row_to_json(d)) INTO resp FROM demo d;
RETURN;
-- end of block X
-- if an exception occurs in block X, then all the changes made within the block are rollback
-- and the control is passed on to the EXCEPTION WHEN OTHERS block.
EXCEPTION
WHEN sqlstate '22013' THEN
resp := json_build_object('code',26);
WHEN sqlstate '22014' THEN
resp := json_build_object('code',22);
END;
$BODY$;
Demo:
Dbfiddle
Related
Info:
No data needed occurs too, when you query a pipelined function that should return n elements. But you have added the condition nb_row< m with m<n.
exemple:
select *
from table ( my_pieplined_function()) -- this function should returns 20 elements
where rownum < 10;
It should occurs with fetch next n rows only. But I've tested that yet.
I have learned that I don't need to handle the exception "no_data_needed" that occurs in a pipelined function. When I'm doing the query, logically oracle catch this exception , take the lines that have been piped and do nothing with the rest.
let oracle catch the exception is equivalent to that.
create my_pieplined_function return sys.odcinumberlist is
i integer:=0;
begin
for loop
pipe row (i);
i:=i+1;
if i=20 then return i; end;
end loop;
exception
when no_data_needed then null;
end;
more info there https://asktom.oracle.com/Misc/nodataneeded-something-i-learned.html
question
I would like to know if there are other exception that are caught by oracle without having to do something. If yes, where can I find the list of these exceptions.
other question :
why don't I find no_data_needed in [summary of predefined exception] (https://docs.oracle.com/cd/B14117_01/appdev.101/b10807/07_errs.htm)
why don't I find no_data_needed in [summary of predefined exception] (https://docs.oracle.com/cd/B14117_01/appdev.101/b10807/07_errs.htm)
Because it was not documented in that version.
It is in the Oracle 11g Database PL/SQL Language Reference.
I have learned that I don't need to handle the exception no_data_needed that occurs in a pipelined function.
You only do not need to handle that exception if you have nothing that needs doing when cleaning up after the pipelined function.
If you do:
CREATE FUNCTION my_pipelined_function
RETURN sys.odcinumberlist PIPELINED
IS
BEGIN
DBMS_OUTPUT.PUT_LINE( 'Initialise' );
FOR i IN 1 .. 2 LOOP
DBMS_OUTPUT.PUT_LINE( 'Loop ' || i );
pipe row (i);
END LOOP;
DBMS_OUTPUT.PUT_LINE( 'Cleanup' );
RETURN;
END;
/
If you call the function then using:
SELECT * FROM TABLE(my_pipelined_function());
Then the output is:
COLUMN_VALUE
1
2
Initialise
Loop 1
Loop 2
Cleanup
But if you limit the number of rows:
SELECT * FROM TABLE(my_pipelined_function()) WHERE rownum = 1;
Then the output will be:
COLUMN_VALUE
1
Initialise
Loop 1
and the Cleanup section is never reached as you stop calling the function in the middle of the loop.
If you define the function as:
CREATE FUNCTION my_pipelined_function
RETURN sys.odcinumberlist PIPELINED
IS
BEGIN
DBMS_OUTPUT.PUT_LINE( 'Initialise' );
FOR i IN 1 .. 2 LOOP
DBMS_OUTPUT.PUT_LINE( 'Loop ' || i );
pipe row (i);
END LOOP;
DBMS_OUTPUT.PUT_LINE( 'Cleanup 1' );
RETURN;
EXCEPTION
WHEN NO_DATA_NEEDED THEN
DBMS_OUTPUT.PUT_LINE( 'Cleanup 2' );
RETURN;
END;
/
and call it using:
SELECT * FROM TABLE(my_pipelined_function()) WHERE rownum = 1;
Then the output will be:
COLUMN_VALUE
1
Initialise
Loop 1
Cleanup 2
So you don't need to handle that exception if your pipelined function has nothing to do when the function finishes; however, if you initialise something and need to clean it up at the end of the function then you should handle the NO_DATA_NEEDED exception as it will allow you to perform the clean up when the loop is terminated early.
db<>fiddle here
I would like to know if there are other exception that are caught by oracle without having to do something.
In the SQL scope (but not the PL/SQL scope), Oracle will silently catch the NO_DATA_FOUND exception and replace it with a NULL value:
For example:
SELECT (SELECT 1 FROM DUAL WHERE 1 = 0) AS value
FROM DUAL;
The outer query expects a value:
SELECT <something> AS value
FROM DUAL;
The inner query:
SELECT 1 FROM DUAL WHERE 1 = 0
Generates zero rows and so no value; this raises a NO_DATA_FOUND exception which is silently caught and replaced by a NULL value.
This can be seen more clearly with:
WITH FUNCTION ndf RETURN NUMBER
IS
BEGIN
RAISE NO_DATA_FOUND;
END;
SELECT ndf() AS value FROM DUAL;
Which outputs:
VALUE
null
The behaviour is different in PL/SQL:
DECLARE
v_value NUMBER;
BEGIN
SELECT 1 INTO v_value FROM DUAL WHERE 1 = 0;
END;
/
Will not catch the NO_DATA_FOUND exception and it will cause the execution of the block to terminate early.
I am writing a procedure to delete department from a table. It takes depatment id as argument and delete the department with given id. but it is not working correctly.When i didnot use EXCEPTION, it only give output when gien department id is present in table but if the id is not present in table it throw error. When i use exception, It did not check the if else condition.
Here is my procedure
CREATE OR REPLACE PROCEDURE del_job(j_id number) IS
jj_id bb_department.iddepartment%type;
BEGIN
SELECT IDDEPARTMENT
INTO jj_id
FROM BB_DEPARTMENT
WHERE IDDEPARTMENT=j_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
jj_id := NULL;
IF (jj_id=j_id) THEN
DELETE FROM BB_DEPARTMENT
WHERE IDDEPARTMENT=j_id;
dbms_output.put_line ('Job Deleted');
ELSIF(jj_id=0) THEN
dbms_output.put_line ('No Job Deleted');
END IF;
END;
/
Your indentation seems to imply that you want your if statement to be part of the normal flow rather than part of the exception block. But your actual code has the if statement in the exception handler. Since you're assigning a null to jj_id in the exception handler before running the if statement and null is never equal to nor unequal to any value, neither your if nor your elsif clause can ever be true so neither dbms_output call will be made.
Assuming your indentation shows your actual intent, my guess is that you want a nested PL/SQL block for the select statement and exception handler.
CREATE OR REPLACE PROCEDURE del_job(j_id number) IS
jj_id bb_department.iddepartment%type;
BEGIN
BEGIN
SELECT IDDEPARTMENT
INTO jj_id
FROM BB_DEPARTMENT
WHERE IDDEPARTMENT=j_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
jj_id := NULL;
END;
IF (jj_id=j_id) THEN
DELETE FROM BB_DEPARTMENT
WHERE IDDEPARTMENT=j_id;
dbms_output.put_line ('Job Deleted');
ELSIF(jj_id=0) THEN
dbms_output.put_line ('No Job Deleted');
END IF;
END;
/
You are making things overly difficult. DELETE sets sql%rowcount to the number of rows processed. So there is no need to select the department; just delete the appropriate id. If you want a confirmation message then test sql%rowcount. If the row was deleted it will contain 1 (or greater), if the id did not exist it will contain 0. Print the appropriate message.
create or replace procedure del_job(j_id number) is
begin
delete
from bb_department
where iddepartment=j_id;
if sql%rowcount > 0 then
dbms_output.put_line ('Job Deleted');
else
dbms_output.put_line ('No Job Deleted');
end if;
end del_job;
/
I have the following PL/SQL code:
BEGIN
FOR c IN (SELECT ...) LOOP
<code1>;
END LOOP;
<code2>;
EXCEPTION WHEN NO_DATA_FOUND THEN
NULL;
END;
This code should run code1 multiple times within a loop and upon finishing this loop code2 should be executed. Otherwise if SELECT query does not find data then I expect this should raise an exception and overstep code2, but this is not happening. Why?
NO_DATA_FOUND is thrown by statements that must return exactly one row but do not find a matching row, e.g.
DECLARE x NUMBER;
BEGIN
SELECT foo INTO x FROM bar WHERE xyz='abc';
EXCEPTION
WHEN NO_DATA_FOUND THEN
...
END;
In your case, you could do the following:
DECLARE foundSomething BOOLEAN := FALSE;
BEGIN
FOR c IN (SELECT ...) LOOP
foundSomething := TRUE;
<code1>;
END LOOP;
IF NOT foundSomething THEN
NULL; -- handle the situation
ELSE
<code2>;
END IF;
END;
No, that's not what is supposed to happen.
If there is no data, then loop runs 0 times - i.e. it skips code1 and executes code2.
You can define explicit cursors and do the checks for data unavailability like this:
DECLARE
cursor cur is select 1 a from dual where 1 = 1;
type tab is table of cur%rowtype;
v tab;
BEGIN
open cur;
loop
fetch cur bulk collect into v;
if v.count = 0 then
raise no_data_found;
end if;
dbms_output.put_line('Code1');
end loop;
close cur;
dbms_output.put_line('Code2');
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('Error');
END;
/
You can easily extends this code to do other things such as divide fetch into batches etc.
I have a set of table names, let say 150. Each table have mail_id column, now I want to search one mail_id in all of the table. For that I wrote one Plsql block. When I loop through the set of table some tables do not exists so it raises an exception. I have exception handling block to handle that exception. Now I want to loop entire table even though it raise an exception? Any idea? Actually my block didn't handle that particular exception!
declare
my_mail_id varchar2(50):='xyaksj#jsm.com';
tmp_table varchar2(125);
type varchar_collector is table of varchar2(255);
var varchar_collector;
table_does_not_exist exception;
PRAGMA EXCEPTION_INIT(table_does_not_exist, -00942);
begin
for cntr in (select table_name from user_tables)
loop
tmp_table:=cntr.table_name;
dbms_output.put_line(tmp_table);
for mail in (select email_address from tmp_table where lower(email_address) like '%my_mail_id%' )
loop
dbms_output.put_line(tmp_table);
end loop;
end loop;
exception
when no_data_found then
dbms_output.put_line('email address not found');
WHEN table_does_not_exist then
dbms_output.put_line('table dose not exists');
WHEN OTHERS THEN
--raise_application_error(-20101, 'Expecting at least 1000 tables');
IF (SQLCODE = -942) THEN
--DBMS_Output.Put_Line (SQLERRM);
DBMS_Output.Put_Line ('in exception');--this exception not handled
ELSE
RAISE;
END IF;
end;
Just handle your exceptions in anonymous block inside the loop.
DECLARE
my_mail_id VARCHAR2(50) := 'xyaksj#jsm.com';
tmp_table VARCHAR2(125);
TYPE varchar_collector IS TABLE OF VARCHAR2(255);
var varchar_collector;
table_does_not_exist EXCEPTION;
PRAGMA EXCEPTION_INIT(table_does_not_exist, -00942);
BEGIN
FOR cntr IN (SELECT table_name FROM user_tables)
LOOP
BEGIN
tmp_table := cntr.table_name;
dbms_output.put_line(tmp_table);
FOR mail IN (SELECT email_address
FROM tmp_table
WHERE lower(email_address) LIKE '%my_mail_id%')
LOOP
dbms_output.put_line(tmp_table);
END LOOP;
EXCEPTION
WHEN no_data_found THEN
dbms_output.put_line('email address not found');
WHEN table_does_not_exist THEN
dbms_output.put_line('table dose not exists');
WHEN OTHERS THEN
--raise_application_error(-20101, 'Expecting at least 1000 tables');
IF (SQLCODE = -942)
THEN
--DBMS_Output.Put_Line (SQLERRM);
DBMS_Output.Put_Line('in exception'); --this exception not handled
ELSE
RAISE;
END IF;
END;
END LOOP;
END;
If you're selecting from user_tables and finding that some of them do not exist then you're probably trying to query tables that are in the recycle bin (their names begin BIN$).
If so, change your query to:
select table_name
from user_tables
where dropped = 'NO';
You should replace your second cursor with a call to execute immediate also, constructing the query by concatenating in the table_name not just using a variable as the table name, and you might as well construct the query as:
select count(*)
from table_name
where lower(email_address) like '%my_mail_id%'
and rownum = 1;
That way you'll retrieve a single record that is either 0 or 1 to indicate whether the email address was found, and no need for error handling.
try below code...
DECLARE
foo BOOLEAN;
BEGIN
FOR i IN 1..10 LOOP
IF foo THEN
GOTO end_loop;
END IF;
<<end_loop>> -- not allowed unless an executable statement follows
NULL; -- add NULL statement to avoid error
END LOOP; -- raises an error without the previous NULL
END;
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;