How to handle exceptions in for loop for insert rows using a stored procedure? Oracle PLSQL - oracle

I'm coding a complex PLSQL block (complex for me hahaha) to insert rows using information from the FOR LOOP CURSOR and add parameters to insert using a stored procedure. The current problem is there are around 200 rows to be inserted but when a simple row fail to insert all rows inserted broke and oracle execute a ROLLBACK command (I think so). So... How could I handle exceptions to insert succefully all rounds I can and when any rows fail show it in screen? Thanks
FOR i IN c_mig_saldos LOOP
IF i.tipo_comprobante = 'P' THEN -- Nota de debito (positivo)
v_cmp_p.prn_codigo := 'VIV';
v_cmp_p.tcm_codigo := 'NRA';
v_cmp_p.cmp_fecha_emision := TRUNC(SYSDATE);
v_cmp_p.cmp_fecha_contable := TRUNC(SYSDATE);
v_cmp_p.cmp_observacion := 'GENERACION AUTOMATICA DE SALDOS';
v_cmp_p.cli_codigo := i.cli_codigo;
v_tab_dco_p(1).cnc_codigo := 'VIA';
v_tab_dco_p(1).dco_precio_unitario := i.total_final;
v_tab_dco_p(1).dco_cantidad := 1;
v_tab_dco_p(1).dco_importe := i.total_final;
-- Insert a new row using stored procedure but when a itereted fail, no rows has inserted in table
PKG_COMPROBANTES.PRC_INSERTAR_COMPROBANTE(v_cmp_p, v_tab_dco_p, v_tab_pgc_p, v_tab_apl_p, v_tab_mar_p);
COMMIT;
END IF;
END LOOP;

-- Insert a new row using stored procedure but when a itereted fail, no rows has inserted in table
begin
PKG_COMPROBANTES.PRC_INSERTAR_COMPROBANTE(v_cmp_p, v_tab_dco_p, v_tab_pgc_p, v_tab_apl_p, v_tab_mar_p);
exception
when others then --this could be made more specific but you didn't say what type of error you were getting
-- Log to a table so you can export failed inserts later.
-- Make sure log table cols are large enough to store everything that can possibly be inserted here...
ErrorLogProc(the, cols, you, want, to, see, and, SQLERRM);
end;
In the ErrorLogProc() I'd recommend a couple things, here is a snippet of some things I do in my error logging proc that you may find helpful (it's just a few snippets, not intended to fully work, but you should get the idea)...
oname varchar2(100);
pname varchar2(100);
lnumb varchar2(100);
callr varchar2(100);
g1B CHAR(2) := chr(13)||chr(10);
PRAGMA AUTONOMOUS_TRANSACTION; --important!
begin
owa_util.who_called_me(oname, pname, lnumb, callr);
--TRIM AND FORMAT FOR ERRORLOG
lv_errLogText := 'Package: '||pname||' // Version: '||version_in||' // Line Number: '||lnumb||' // Error: ';
lv_string1 := mid(errStr_in,1,4000-Length(lv_errLogText));
lv_errLogText := lv_errLogText||lv_string1;
lv_errLogText := lv_errLogText||g1B||'Error Backtrace: '||replace(dbms_utility.format_error_backtrace,'ORA-', g1b||'ORA-');
insertIntoYourErrorLogTable(lv_errLogText, and, whatever, else, you, need);
commit;

To keep this simple, since there's not enough information to know the what and why of the question, this will kick out some text information about failures as desired.
SQL> set serveroutput on
Then here's an anonymous PL/SQL block:
BEGIN
FOR i IN c_mig_saldos
LOOP
-- not relevant
BEGIN
PKG_COMPROBANTES.PRC_INSERTAR_COMPROBANTE(v_cmp_p, v_tab_dco_p, v_tab_pgc_p, v_tab_apl_p, v_tab_mar_p);
EXCEPTION
-- The goal of this is to ignore but output information about your failures
WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('whatever you want about v_cmp_p, v_tab_dco_p, v_tab_pgc_p, v_tab_apl_p, v_tab_mar_p or SQLERRM/SQLCODE or the error stack - not enough info in the question');
END;
END LOOP;
END;
/
Note: I have removed the commit from the execution.
Then if you like what you see...
SQL> commit;
Ideally, if I knew more about why the insert failures were expected to occur and what I wanted to do about them, I would not insert them in the first place.

Agree with comment that more information is needed, but a couple of things to consider:
Does this need to be done as a loop - if you can write your query as a select statement, then you can do a simple insert without the need for PLSQL, which would be simpler and likely quicker (set based SQL vs row-by-row PLSQL)
You say a ROLLBACK is occuring - you have a commit inside your IF statement, so any records which make it into your IF statement and succesfully make it through your insert procedure will be committed i.e. they will not be rolled back; You should consider if some records you think are being rolled back are actually not making it into the IF statement at all
Can you provide example data, or an example of the error you are receiving?

Related

PLSQL Loop through query finds all the records

i'm facing a problem with a stored procedure, the thing is i'm trying to find if the transaction number in the temporary table is already in the final table, if not it will insert the record, if it's in the final table, it's going to a log_error table, here's my SP
BEGIN
DECLARE
date temporary_table.transfer_date%TYPE;
auth temporary_table.auth_code%TYPE;
transac_num temporary_table.transaction_number%TYPE;
card temporary_table.card_number%TYPE;
amount temporary_table.amount%TYPE;
num_trx_search NUMBER;
counter NUMBER;
sid1 NUMBER;
sid2 NUMBER;
loopcounter NUMBER;
BEGIN
cod_error := 0;
warning := 'execution';
OPEN vocursor FOR
SELECT transfer_date,
auth_code,
transaction_number,
card_number,
amount
FROM temporary_table order by id;
prfcursor := vocursor;
OPEN ntxcursor FOR
SELECT transaction_number FROM final_table order by id;
trxcursor := ntxcursor;
LOOP
FETCH prfcursor INTO date, auth, transac_num, card, amount;
EXIT WHEN prfcursor%NOTFOUND;
FETCH trxcursor INTO num_trx_search;
dbms_output.Put_line('NumTrx: ' || num_trx);
begin
-- i need to check if the transaction number from the temporary table is already in the
--final table
FOR loopcounter IN (Select id from final_table where transaction_number = transac_num)
LOOP
DBMS_OUTPUT.PUT_LINE(loopcounter.sid);
END LOOP;
dbms_output.Put_line('num_trx_search: ' || num_trx_search);
dbms_output.Put_line('counter: ' || counter);
exception
WHEN NO_DATA_FOUND THEN
dbms_output.Put_line('No Data found');
end;
EXIT WHEN trxcursor%NOTFOUND;
--just for testing and debuging
counter := 1;
IF(counter > 0) THEN
--inserts into log error table
ELSE
--inserts into final table
END IF;
END LOOP;
dbms_output.Put_line( 'end loop' );
CLOSE trxcursor;
CLOSE prfcursor;
dbms_output.Put_line( 'end cursor' );
END;
The thing is, it's getting all the results, for each record in the temporary, should get just one if the transaction number matches.
NumTrx is the transaction number in the temporary table.
I'm a noob in plsql, thanks
You could achieve the same thing by trying to insert the records from the temporary table into the final table and use the LOG ERRORS INTO clause to push those records that are already in final into another table.
INSERT INTO final_table final
SELECT transfer_date,
auth_code,
transaction_number,
card_number,
amount
FROM temporary_table
LOG ERRORS INTO ERR$_final_table
The query above assumes that final_table and temporary_table have the same structure. If they are different you will need to adjust the query slightly. Generally you should try to do as much of what you want in a single SQL rather than writing lots of procedural code to achieve the same thing. It is usually quicker and in this case appears to be much simpler.
For the set up of the ERR$ table I suggest you look at the Oracle docs under DML ERROR LOGGING.
If you do wish to do a row-by-row (slow) update then I would suggest using implicit cursor for loops instead simply for readability. Also I don't think the ORDER BY on each cursor is going to do anything except slow your code down.

how to insert the result of dbms_output.put_line to a table for error catch?

Exception
WHEN OTHERS THEN
--dbms_output.put_line('pl_update_sidm_user r: ERROR CODE:' || sqlcode || '~' ||
--sqlerrm || ' EMPL_NBR:' || r.EMPL_NBR);
insert into ERROR_MSG (ERROR_MSG_ID,ERROR_MSG) values (ERROR_MSG_ID_SEQ.NEXTVAL, 'pl_update_sidm_user_duty_role r2');
END;
I would like to put the error result to a table.
However, how can I do that?
Can I put the result of dbms_output to a table as a string?
If not, can I get the sqlcode,sqlerrm without using dbms_output?
Thank you !!
From the documentation,
A SQL statement cannot invoke SQLCODE or SQLERRM. To use their values
in a SQL statement, assign them to local variables first
Also,
Oracle recommends using DBMS_UTILITY.FORMAT_ERROR_STACK except when using the FORALL statement with its SAVE EXCEPTIONS clause
So, for SQLCODE or SQLERRM, you should assign them into variables and use them.
DECLARE
v_errcode NUMBER;
v_errmsg VARCHAR2(1000);
BEGIN
--some other statements that may raise exception.
EXCEPTION
WHEN OTHERS THEN
v_errcode := SQLCODE;
v_errmsg := SQLERRM;
insert into ERROR_TABLE (ERROR_MSG_ID,ERROR_MSG) --change your table name
values (ERROR_MSG_ID_SEQ.NEXTVAL,
v_errcode||':'||v_errmsg);
END;
/
Preferably use insert like this instead, as per Oracle's recommendation.
insert into ERROR_TABLE (ERROR_MSG_ID,ERROR_MSG) values (ERROR_MSG_ID_SEQ.NEXTVAL,
DBMS_UTILITY.FORMAT_ERROR_STACK);
Demo
Technically what others are suggesting is correct: the "insert" operation executed in the "exception when others" block will actually insert a new row in the log table.
the problem is that such insert statement will be part of the same transaction of the main procedure and, since you had an error while executing it, you are very likely to rollback that transaction, and this will rollback also the insert in your log table
I suppose the problem you are facing is not that you aren't successfully logging the error message: it is that you are rolling it back immediately afterwards, along with all the other writes you did in the same transaction.
Oracle gives you a way of executing code in a SEPARATE transaction, by using "autonomous transaction" procedures.
you need to create such a procedure:
create or replace procedure Write_Error_log(
arg_error_code number,
arg_error_msg varchar2,
arg_error_backtrace varchar2) is
PRAGMA AUTONOMOUS_TRANSACTION;
begin
INSERT INTO error_msg (
error_msg_id,
error_code,
error_msg,
error_stack)
VALUES (
error_msg_id_seq.NEXTVAL,
arg_error_code,
arg_error_msg,
arg_error_backtrace);
commit; -- you have to commit or rollback always, before exiting a
-- pragma autonomous_transaction procedure
end;
What this procedure does is to write a new record in the log table using a totally separate and independent transaction: the data will stay in the log table even if you execute a roll back in your calling procedure. You can also use such a procedure to create a generic log (not only errors).
All you have to do now is to call the procedure above whenever you need to log something, so your code becomes:
DECLARE
v_errcode NUMBER;
v_errmsg VARCHAR2(1000);
BEGIN
--some other statements that may raise exception.
EXCEPTION WHEN OTHERS THEN
Write_Error_log(SQLCODE, SQLERRM, dbms_utility.format_error_backtrace);
END;
/
P.S: there might be some typos in my code: I can't test it right now since I can't reach an oracle server in this moment.

Oracle 11g: In PL/SQL is there any way to get info about inserted and updated rows after MERGE DML statement?

I would like to know is there any way to receive information in PL/SQL how many rows have been updated and how many rows have been inserted while my PL/SQL script using MERGE DML statement.
Let's use Oracle example of merge described here: MERGE example
This functionality is used in my function but also I'd like to log information how many rows has beed updated and how many rows have been inserted.
There is a not a built-in way to get separate insert and update counts, no. SQL%ROWCOUNT would tell you the number of rows merged, as you probably already know, but there is no equivalent to get separate values for the inserts and updates.
This article by Adrian Billington shows a way to get the information by including a function call in the merge, which might add a little overhead.
There's a similar, and perhaps simpler, trick from MichaelS on the Oracle forums, which I can't take any credit for at all either, of course. I'm tempted to reproduce it here but I'm not sure if that's allowed, but essentially it's using sys_context to maintain a count, in much the same way that Adrian's solution did with a package variable. I'd use that one, as it's cleaner and I think it's easier to follow and maintain.
Still perilously close to a link-only answer but I don't want to plagiarise others' work either...
Workarounds pointed by #AlexPoole works, but for me it's strange why don't count updates, inserts and even possible deletes by more natural way with triggers.
Suppose we have simple test table:
create table test_table (id number, col number)
Define simple package for counters
create or replace package pkg_test_table_counter as
procedure reset_counter;
procedure count_insert;
procedure count_update;
procedure count_delete;
function get_insert_count return number;
function get_update_count return number;
function get_delete_count return number;
end;
... and package body:
create or replace package body pkg_test_table_counter as
vUpdateCount number := 0;
vInsertCount number := 0;
vDeleteCount number := 0;
procedure reset_counter is
begin
vUpdateCount := 0;
vInsertCount := 0;
vDeleteCount := 0;
end;
procedure count_insert is
begin
vInsertCount := vInsertCount + 1;
end;
procedure count_update is
begin
vUpdateCount := vUpdateCount + 1;
end;
procedure count_delete is
begin
vDeleteCount := vDeleteCount + 1;
end;
function get_insert_count return number is
begin
return vInsertCount;
end;
function get_update_count return number is
begin
return vUpdateCount;
end;
function get_delete_count return number is
begin
return vDeleteCount;
end;
end;
To count number of rows during execution of single DML statement we need to reset it in before statement trigger
create or replace trigger trg_test_table_counter_reset
before insert or update or delete on test_table
begin
pkg_test_table_counter.reset_counter;
end;
... and increment appropriate counter in trigger for each row:
create or replace trigger trg_test_table_counter_count
before insert or update or delete on test_table
for each row
begin
if inserting then
pkg_test_table_counter.count_insert;
end if;
if updating then
pkg_test_table_counter.count_update;
end if;
if deleting then
pkg_test_table_counter.count_delete;
end if;
end;
So, after executing MERGE statement without additional tricks inside DML query text it's always possible to get exact number of affected rows:
select
pkg_test_table_counter.get_insert_count insert_count,
(
pkg_test_table_counter.get_update_count
-
pkg_test_table_counter.get_delete_count
) update_count,
pkg_test_table_counter.get_delete_count delete_count
from dual
Note that delete operations also counted as updates for MERGE , but to keep package useful for another operations there are two separate counters.
SQLFiddle test

How to display output from stored procedure?

I am handling millions of rows and it takes hours, so I want some feedback to give me an idea about the status of the process. It won't be possible to have real time feedback from a stored procedure since the output will be available only after the complete execution. Any solution?
There are a couple of solutions.
One is to write to a log of some description. You can use a file (writing out with UTL_FILE) or a table (using autonomous transactions so that the records are visible in another session without affecting the main transaction).
The other solution is to write to the V$SESSION_LONGOPS view using DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS. Find out more
I think logging is always a good idea with long-running background procedures. If something goes wrong your logs are the only source of info you have.
You could have a ReportStatus procedure that writes to a different table (one you can select from while your procedure is running).
It would need a PRAGMA AUTONOMOUS_TRANSACTION so it can commit independently of your main procedure.
Example:
CREATE OR REPLACE PROCEDURE ReportStatus(status NUMBER)
AS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO StatusTable VALUES(SYSDATE, status);
COMMIT;
END ReportStatus;
/
One more way to get feedback from running PL/SQL block is to use inter-session communication. In order to use this solution you need at least two sessions.
In audit (first) session:
set head off arrays 1 pages 0 feedback off timing off
select column_value notification from table (notificationReceive);
It's going to be stuck there until first message arrives.
As the next step execute your PL/SQL block in the another (second) session:
begin
notificationSend ('START: '||'myBlockName');
/* UPDATE table1 SET col1 = value ... WHERE some condition;
*/ dbms_lock.sleep(3);
notificationSend ('Updated 1000000 rows'); -- sql%rowcount instead of literal
/* INSERT INTO table2 (val1, SYSDATE, v_user_id);
*/ dbms_lock.sleep(2);
notificationSend ('Inserted 1000000 rows');
notificationSend ('ENDTX');
end;
/
Now, you can go back to the first session and see the real-time feedback from running PL/SQL block:
11:13:39: START: myBlockName
11:13:42: Updated 1000000 rows
11:13:44: Inserted 1000000 rows
Below the code of almost one line functions:
create or replace procedure notificationSend (message varchar2) is
pragma autonomous_transaction;
begin
dbms_alert.signal ('feedback$_queue', message);
commit;
end;
/
create or replace function notificationReceive return sys.odciVarchar2List pipelined is
pragma autonomous_transaction;
message varchar2(1800) := 'NONE';
status number := -1;
begin
dbms_alert.register('feedback$_queue');
<<reading>> loop
dbms_alert.waitone('feedback$_queue', message, status);
if status = 0 and message != 'ENDTX' then
pipe row (to_char (sysdate, 'hh:mi:ss')||': '||message);
pipe row (null); -- dummy row for prefetch in sqlplus
else
exit reading;
end if;
end loop reading;
dbms_alert.remove('feedback$_queue');
return;
end;
/
Tested with releases 11.2.0.4.0, 12.2.0.1.0.

How do I unit test this PL/SQL procedure? (using utplsql)

Package is very very basic.
Loops through a cursor, and updates 2 values where the record_ids are equal.
What's an appropriate unit test for this sort of procedure?
I'm going to add some skeleton code because the answers so far, while good, tie to the crux of my issue here: What do I test?
PROCEDURE set_shift_times_to_null( RETVAL OUT VARCHAR2,
ERRBUF OUT VARCHAR2,
RECORDS_UPDATED OUT NUMBER) IS
CURSOR evening_shift_employees_cur IS
select employee
FROM employees
where SHIFT='EVENING'
;
BEGIN
RECORDS_UPDATED := 0;
RETVAL := '2';
FOR evening_shift_employees IN evening_shift_employees_cur LOOP
UPDATE NIGHT_SHIFT
Set SOME_DUMB_FIELD = evening_shift_employees.employee;
RECORDS_UPDATED := RECORDS_UPDATED + 1;
END LOOP;
COMMIT;
RETVAL := 0;
EXCEPTION WHEN OTHERS THEN
ROLLBACK;
ERRBUF := 'Error occurred - ' || SQLERRM;
END set_shift_times_to_null;
A couple of suggestions.
Use SQL%ROWCOUNT:
BEGIN
UPDATE NIGHT_SHIFT
Set SOME_DUMB_FIELD = evening_shift_employees.employee;
v_rows_processed := SQL%ROWCOUNT;
dbms_output.put_line('There were '||v_rows_processed||' rows updated');
END;
Don't Use When Others (why do you want to lose the stack trace).Just use Exceptions, you will be relying on the caller to check the contents of the ERRBUF.
begin
insert into t values ( 1 );
exception when others then
log_error;
raise;
end;
log_error implementation looks like:
create or replace procedure log_error
as
pragma autonomous_transaction;
l_whence varchar2(1024);
l_msg varchar2(1020) default sqlerrm;
l_code number default sqlcode;
begin
l_whence := whence;
insert into error_table
( timestamp, whence, msg, code )
values
( sysdate, whence, l_msg, l_code );
commit;
exception
when others then
rollback;
raise;
end;
Consider not using any pl/sql. on the surface the update appears completely 'doable' without any cursor. Perhaps an updateable inline view:
update (
select e.sal as emp_sal, e.comm as emp_comm,
ns.sal as ns_sal, ns.sal/2 as ns_comm
from employees e, night_shift ns
where e.deptno = ns.deptno
)
set emp_sal = ns_sal, emp_comm = ns_comm
The appropriate unit test it to validate the affected tables to check that the updated records are what expected.
You can create temporary tables with the results you expect and the unit testing code compare the results. Of course is hard work but if you want to test you have to do something like this.
It depends on the work of procedure, but if you want to be sure that test is fine you have to check as possibilities as possible.
A lot of the conditions have to be validated with constraints and the test unit procedures have to execute code that force the database to check that constraints (inserts, and so on).
What I ended up doing was the following:
Take record count of records that main cursor will update fields to null
execute procedure ( returns a value of rows updated )
Take record count of same records as first time
The difference between the record counts should equal the number of records updated in the procedure. If so, the test passes.
Does this make sense, or is it circular logic?
For anyone else who sees this, I found this in the documentation for utplsql:
PROCEDURE utAssert.eqtable (
msg_in IN VARCHAR2,
check_this_in IN VARCHAR2,
against_this_in IN VARCHAR2,
check_where_in IN VARCHAR2 := NULL,
against_where_in IN VARCHAR2 := NULL,
raise_exc_in IN BOOLEAN := FALSE
);
It's under the assert documentation; looks like it does exactly what I was trying to do.
Basically, you want to exercise all the possibilities of your procedure:
Test with equal record-ids.
Test with non-equal record-ids.
Test with invalid (non-integer? negative?) record-ids.
Also, test the boundary conditions:
Test with record-ids off by one (ex: 104 and 105).
Test with maximum record-id (MAX_INT?).
Test with zero-value record-id.
Here is a nice example of good unit-testing practices.
EDIT: I don't know of a robust unit-testing tool for database queries. I would set up a test table of evening_shift_employees with various record-id conditions as described above. Then, as suggested by FerranB, check that the records are updated as expected for validation.

Resources