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

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.

Related

How can I test my package with one function?

I made a package that compiles fine but when I try to test it it gives me "invalid data type".
I've tried two different ways, first one like this
select pkg_contabilidad.f_totalizar_Detalle(100) FROM DUAL;
It gives me the ORA-00902 'invalid data type'
Also I've tried this
DECLARE
TYPE r_registro IS RECORD
(rubro_contable CN_RUBROS_CONTABLES.COD_RUBRO%TYPE,
tipo VARCHAR2(1),
monto NUMBER(16));
resultao r_registro;
numero NUMBER :=100;
BEGIN
resultao := pkg_contabilidad.f_totalizar_detalle(numero);
END;
It gives me another error PLS-00382 'expression is of wrong type'
I don't know what am I doing wrong, cause my function receives just one parameter and is of type NUMBER, so I dont know where's my mistake. I'll leave the code of my package just in case
CREATE OR REPLACE PACKAGE pkg_contabilidad AS
TYPE r_registro IS RECORD
(rubro_contable CN_RUBROS_CONTABLES.COD_RUBRO%TYPE,
tipo VARCHAR2(1),
monto NUMBER(16));
TYPE t_detalle IS TABLE OF
r_registro INDEX BY BINARY_INTEGER;
FUNCTION f_totalizar_detalle(p_clave NUMBER)RETURN t_detalle;
END pkg_contabilidad;
/
CREATE OR REPLACE PACKAGE BODY pkg_contabilidad AS
B_detalle t_detalle;
i integer :=1;
FUNCTION f_totalizar_detalle(p_clave NUMBER) RETURN t_detalle IS
v_detalle t_detalle;
CURSOR c_facturado IS
SELECT c.cod_rubro, 'H', CASE WHEN SUM(d.gravada)=0 THEN SUM(d.iva) ELSE SUM(d.gravada) END
FROM fn_documentos_det d JOIN fn_conceptos c ON d.cod_concepto = c.cod_concepto
WHERE d.clave_doc=p_clave
GROUP BY c.cod_rubro;
CURSOR c_datos IS
SELECT SUM(d.total_doc), 'D',r.cod_rubro
FROM fn_documentos d JOIN fn_cajas_ctas r ON d.num_caja_cta = r.num_caja_cta
WHERE d.clave_doc = p_clave
GROUP BY r.cod_rubro;
BEGIN
open c_datos;
LOOP
FETCH c_datos INTO v_detalle(1);
END LOOP;
CLOSE c_datos;
FOR fila IN c_facturado LOOP
i := i + 1;
v_detalle(i) := fila;
END LOOP;
END;
END PKG_CONTABILIDAD;
The function returns a pkg_contabilidad.t_detalle, so the test needs to be:
declare
resultao pkg_contabilidad.t_detalle;
numero number := 100;
begin
resultao := pkg_contabilidad.f_totalizar_detalle(numero);
end;
It doesn't work in SQL because pkg_contabilidad.t_detalle is a PL/SQL type, not a SQL type (create or replace type). The database can perform some automatic conversions, but there are still limitations.
By the way, this loop will never complete because it lacks an exit condition:
open c_datos;
loop
fetch c_datos into v_detalle(1);
end loop;
close c_datos;
Your function returns a PL/SQL table type, with a table of a PL/SQL record type, which is defined in your package, which plain SQL doesn't know about and can't display - hence your invalid datatype error. If you need to call the function and access the data from SQL you can create schema-level object and collection types instead.
In your anonymous block you are a declaring a new record type. That looks the same to you because the structure is the same, but Oracle expects the exact type the function returns. That makes your test code shorter and simpler though. But you are also trying to return the whole collection into a single record.
DECLARE
l_detalle pkg_contabilidad.t_detalle;
l_registro pkg_contabilidad.r_registro;
l_idx pls_integer;
numero NUMBER :=100;
BEGIN
l_detalle := pkg_contabilidad.f_totalizar_detalle(numero);
l_idx := l_detalle.FIRST;
WHILE l_idx is not null LOOP
l_registro := l_detalle(l_idx);
-- do something with this record
dbms_output.put_line(l_registro.tipo);
l_idx := l_detalle.NEXT(l_idx);
END LOOP;
END;
db<>fiddle with dummy cursors.
Your function is a bit strange and probably isn't doing quite what you want; but also has two fatal problems: it isn't returning anything, and it has an infinite loop. I've fixed those for the fiddle but not anything else, as this seems to be an exercise.

How to display the results of a procedure outside of it in Oracle

I am working on an application and made the decision that all the queries would be procedures. I hope to have gains in performance and ease of maintenance by doing it this way. Our DBA's have also expressed interest in having it done this way.
I have an HR table where operations are performed on it each night, and any changes are recorded in a secondary table. We are not doing auditing, these change records are kept until the next run and show users the changes that have happened.
To keep my question shorter I have reduced the number of columns in HR.
The HR table ID, GROUP_NAME, and GROUP_LEVEL. The Drill table has ID and TYPEVALUE.
CREATE OR REPLACE PROCEDURE DOCSADM.DRILL_RECORD_POSITION (
RECORD_TYPE IN VARCHAR2,
OUT_ID OUT VARCHAR2,
OUT_GROUP_NAME OUT VARCHAR2,
OUT_GROUP_LEVEL OUT VARCHAR2
) AS
BEGIN
SELECT HR.ID, HR.GROUP_NAME, HR.GROUP_LEVEL
INTO OUT_ID, OUT_GROUP_NAME, OUT_GROUP_LEVEL
FROM HR_POSITION HR JOIN DRILL_POSITION DP ON (HR.ID = DP.ID) WHERE DP.TYPEVALUE = RECORD_TYPE;
END DRILL_RECORD_POSITION;
The procedure compiles without issue. Before doing all the work in the application to link to the procedure and extract the values which in this case will eventually be displayed in a view or webpage, I wanted to have a quick little script that would call the procedure and then display the results so I can verify in Oracle.
Loops
BEGIN
for t in (DRILL_RECORD_POSITION('D', V1,V5,V6))
loop
--dbms_output.put_line(t.V1 || t.V5 || t.V6);
dbms_output.put_line(t.OUT_ID);
end loop;
END;
/
CURSORS
DECLARE
V1 HR_POSITION.ID%TYPE;
V5 HR_POSITION.GROUP_NAME%TYPE;
V6 HR_POSITION.GROUP_LEVEL%TYPE;
CURSOR T_CUR IS DRILL_RECORD_POSITION('D', V1,V5,V6);
BEGIN
OPEN T_CUR;
DBMS_OUTPUT.PUTLINE('START');
LOOP
FETCH T_CUR INTO V1,V5,V6;
EXIT WHEN T_CUR%NOTFOUND;
DBMS_OUTPUT.PUTLINE(V1||V5||V6);
END LOOP;
CLOSE T_CUR;
END;
FOR LOOPS
DECLARE
V1 HR_POSITION.POSITION_ID%TYPE;
V5 HR_POSITION.GROUP_NAME%TYPE;
V6 HR_POSITION.GROUP_LEVEL%TYPE;
BEGIN
DBMS_OUTPUT.PUTLINE('START');
FOR INDEX IN (DRILL_RECORD_POSITION('D', V1,V5,V6))
LOOP
--DBMS_OUTPUT.PUTLINE(INDEX.ID);
DBMS_OUTPUT.PUTLINE(INDEX.V1||INDEX.V5||INDEX.V6);
END LOOP;
END;
Note: I edited the column names out and shorted some when transferring here so I might have made a few mistakes.
All the articles I have seen online show me how to display from within the original procedure or by using views, cursors, records. Unless I am wrong, Eclipse wont have any problems using the information in the current form which is why I am passing it that way. So I am not interested in changing the procedure and would like to work with it as is, since thats how the application will be doing it.
As this is the first of the stored procedures I am doing for the application, instead of using adhoc queries from the application, I dont have any existing examples to work from, which is why I believe the results will work fine, because it should be the same format the adhoc ones use.
Update:
In one of the comments, I was pointed to what should have been a solution. This was confirmed by another solution that was under it.
I keep getting the error
ORA-01422: exact fetch returns more than requested number of rows
So Im returning multiple rows, but that is my expectation and what is happening. I just cant seem to figure out how to display the results.
To test the procedure you showed, you would do something like:
declare
l_id hr_position.id%type;
l_group_name hr_position.group_name%type;
l_group_level hr_position.group_level%type;
begin
drill_record_position('D', l_id, l_group_name, l_group_level);
dbms_output.put_line(l_id ||':'|| l_group_name ||':'|| l_group_level);
end;
/
But that - or more specifically, your procedure - only works if there is exactly one row in the query's result set for the passed-in value type. It seems you're expecting multiple rows back (which would get too-many-rows), but there could also be non (which would get no-data-found).
So really it seems like your question should be about how to write your procedure so it works with one of the retrieval/test methods you tried.
If your procedure needs to return multiple rows then it can use a ref cursor, e.g.:
create or replace procedure drill_record_position (
p_record_type in varchar2,
p_ref_cursor out sys_refcursor
)
as
begin
open p_ref_cursor for
select hr.id, hr.group_name, hr.group_level
from hr_position hr
join drill_position dp
on hr.id = dp.id
where dp.typevalue = p_record_type;
end drill_record_position;
/
which you could then test with something like:
declare
l_ref_cursor sys_refcursor;
l_id hr_position.id%type;
l_group_name hr_position.group_name%type;
l_group_level hr_position.group_level%type;
begin
drill_record_position('D', l_ref_cursor);
loop
fetch l_ref_cursor into l_id, l_group_name, l_group_level;
exit when l_ref_cursor%notfound;
dbms_output.put_line(l_id ||':'|| l_group_name ||':'|| l_group_level);
end loop;
close l_ref_cursor;
end;
/
You can also do that as a function, which might be easier to work with from your application:
-- drop procedure drill_record_position;
create or replace function drill_record_position (p_record_type in varchar2)
return sys_refcursor as
l_ref_cursor sys_refcursor;
begin
open l_ref_cursor for
select hr.id, hr.group_name, hr.group_level
from hr_position hr
join drill_position dp
on hr.id = dp.id
where dp.typevalue = p_record_type;
return l_ref_cursor;
end drill_record_position;
/
declare
l_ref_cursor sys_refcursor;
l_id hr_position.id%type;
l_group_name hr_position.group_name%type;
l_group_level hr_position.group_level%type;
begin
l_ref_cursor := drill_record_position('D');
loop
fetch l_ref_cursor into l_id, l_group_name, l_group_level;
exit when l_ref_cursor%notfound;
dbms_output.put_line(l_id ||':'|| l_group_name ||':'|| l_group_level);
end loop;
close l_ref_cursor;
end;
/
You coudl also do this with collections and a pipelined function, which is more work to set up:
create type t_drill_obj as object (
-- use your real data types...
id number,
group_name varchar2(10),
group_level number
)
/
create type t_drill_tab as table of t_drill_obj
/
create or replace function drill_record_position (p_record_type in varchar2)
return t_drill_tab pipelined as
begin
for l_row in (
select t_drill_obj(hr.id, hr.group_name, hr.group_level) as obj
from hr_position hr
join drill_position dp
on hr.id = dp.id
where dp.typevalue = p_record_type
)
loop
pipe row (l_row.obj);
end loop;
return;
end drill_record_position;
/
but you could call it as part of another query, and even join tot he result if you needed to:
select * from table(drill_record_position('D'));

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

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?

Oracle raise_application_error error number best practice

I have a question regarding the error codes (-20000 to -20999) for Raise Application Error.
Can we use same error code (eg -20000) only for different error scenarios at multiple places in PLSQL code?
If we can use same error code in all places, why do we have 1000 codes?
What is the best practice to use error codes in Raise Application Error?
Sample Code:
create table t(id number primary key);
declare
begin
insert into t(id) values (1);
insert into t(id) values (1);
commit;
exception
when dup_val_on_index
then
raise_application_error(-20000, 'Cannot Insert duplicates');
when others
then
raise_application_error(-20000, sqlcode||'-'||sqlerrm);
end;
Can we use same error code (eg -20000) only for different error scenarios at multiple places in PLSQL code?
As Justin notes, you can certainly do that - just use one code. But it is likely to lead to confusion. I've seen it done, and usually in that case, the developers simply embed all critical information into the message, even including a code (they might, for example, already be using their own error codes that fall outside the acceptable range).
I suggest you follow Oracle's lead: assign ranges to areas of your application and then use error codes within a range when an application-specific error occurs in that part of the part.
If we can use same error code in all places, why do we have 1000 codes?
See above.
What is the best practice to use error codes in Raise Application Error?
Create a table in which you "register" error codes that are used, along with the message. Then developers can check to see "their" error is already registered and can re-use it. Or, more likely, they register a new error code and message.
Either way, you have a central point from which to organize the codes and hopefully minimize the change of two developers using the same error code.
Here's a script that does what I suggested above, along with a utility to generate a package with all of the errors defined and available for "soft-coded" reference.
CREATE TABLE msg_info (
msgcode INTEGER,
msgtype VARCHAR2(30),
msgtext VARCHAR2(2000),
msgname VARCHAR2(30),
description VARCHAR2(2000)
);
CREATE OR REPLACE PACKAGE msginfo
IS
FUNCTION text (
code_in IN INTEGER
, type_in IN VARCHAR2
, use_sqlerrm IN BOOLEAN := TRUE
)
RETURN VARCHAR2;
FUNCTION name (code_in IN INTEGER, type_in IN VARCHAR2)
RETURN VARCHAR2;
PROCEDURE genpkg (
NAME_IN IN VARCHAR2
, oradev_use IN BOOLEAN := FALSE
, to_file_in IN BOOLEAN := TRUE
, dir_in IN VARCHAR2 := 'DEMO' -- UTL_FILE directory
, ext_in IN VARCHAR2 := 'pkg'
);
END;
/
CREATE OR REPLACE PACKAGE BODY msginfo
IS
FUNCTION msgrow (code_in IN INTEGER, type_in IN VARCHAR2)
RETURN msg_info%ROWTYPE
IS
CURSOR msg_cur
IS
SELECT *
FROM msg_info
WHERE msgtype = type_in AND msgcode = code_in;
msg_rec msg_info%ROWTYPE;
BEGIN
OPEN msg_cur;
FETCH msg_cur INTO msg_rec;
CLOSE msg_cur;
RETURN msg_rec;
END;
FUNCTION text (
code_in IN INTEGER
, type_in IN VARCHAR2
, use_sqlerrm IN BOOLEAN := TRUE
)
RETURN VARCHAR2
IS
msg_rec msg_info%ROWTYPE := msgrow (code_in, type_in);
BEGIN
IF msg_rec.msgtext IS NULL AND use_sqlerrm
THEN
msg_rec.msgtext := SQLERRM (code_in);
END IF;
RETURN msg_rec.msgtext;
END;
FUNCTION NAME (code_in IN INTEGER, type_in IN VARCHAR2)
RETURN VARCHAR2
IS
msg_rec msg_info%ROWTYPE := msgrow (code_in, type_in);
BEGIN
RETURN msg_rec.msgname;
END;
PROCEDURE genpkg (
NAME_IN IN VARCHAR2
, oradev_use IN BOOLEAN := FALSE
, to_file_in IN BOOLEAN := TRUE
, dir_in IN VARCHAR2 := 'DEMO'
, ext_in IN VARCHAR2 := 'pkg'
)
IS
CURSOR exc_20000
IS
SELECT *
FROM msg_info
WHERE msgcode BETWEEN -20999 AND -20000 AND msgtype = 'EXCEPTION';
-- Send output to file or screen?
v_to_screen BOOLEAN := NVL (NOT to_file_in, TRUE);
v_file VARCHAR2 (1000) := name_in || '.' || ext_in;
-- Array of output for package
TYPE lines_t IS TABLE OF VARCHAR2 (1000)
INDEX BY BINARY_INTEGER;
output lines_t;
-- Now pl simply writes to the array.
PROCEDURE pl (str IN VARCHAR2)
IS
BEGIN
output (NVL (output.LAST, 0) + 1) := str;
END;
-- Dump to screen or file.
PROCEDURE dump_output
IS
BEGIN
IF v_to_screen
THEN
FOR indx IN output.FIRST .. output.LAST
LOOP
DBMS_OUTPUT.put_line (output (indx));
END LOOP;
ELSE
-- Send output to the specified file.
DECLARE
fid UTL_FILE.file_type;
BEGIN
fid := UTL_FILE.fopen (dir_in, v_file, 'W');
FOR indx IN output.FIRST .. output.LAST
LOOP
UTL_FILE.put_line (fid, output (indx));
END LOOP;
UTL_FILE.fclose (fid);
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line ( 'Failure to write output to '
|| dir_in
|| '/'
|| v_file
);
UTL_FILE.fclose (fid);
END;
END IF;
END dump_output;
BEGIN
/* Simple generator, based on DBMS_OUTPUT. */
pl ('CREATE OR REPLACE PACKAGE ' || NAME_IN);
pl ('IS ');
FOR msg_rec IN exc_20000
LOOP
IF exc_20000%ROWCOUNT > 1
THEN
pl (' ');
END IF;
pl (' exc_' || msg_rec.msgname || ' EXCEPTION;');
pl ( ' en_'
|| msg_rec.msgname
|| ' CONSTANT INTEGER := '
|| msg_rec.msgcode
|| ';'
);
pl ( ' PRAGMA EXCEPTION_INIT (exc_'
|| msg_rec.msgname
|| ', '
|| msg_rec.msgcode
|| ');'
);
IF oradev_use
THEN
pl (' FUNCTION ' || msg_rec.msgname || ' RETURN INTEGER;');
END IF;
END LOOP;
pl ('END ' || NAME_IN || ';');
pl ('/');
IF oradev_use
THEN
pl ('CREATE OR REPLACE PACKAGE BODY ' || NAME_IN);
pl ('IS ');
FOR msg_rec IN exc_20000
LOOP
pl (' FUNCTION ' || msg_rec.msgname || ' RETURN INTEGER');
pl (' IS BEGIN RETURN en_' || msg_rec.msgname || '; END;');
pl (' ');
END LOOP;
pl ('END ' || NAME_IN || ';');
pl ('/');
END IF;
dump_output;
END;
END;
/
/* Sample data to be used in package generation. */
BEGIN
INSERT INTO msg_info
VALUES (-20100, 'EXCEPTION', 'Balance too low', 'bal_too_low'
, 'Description');
INSERT INTO msg_info
VALUES (-20200, 'EXCEPTION', 'Employee too young', 'emp_too_young'
, 'Description');
COMMIT;
END;
/
Can you use the same error code every time? Sure.
Should you? Almost certainly not. It would be rather annoying if Oracle raised an ORA-00001 error for every possible thing that went wrong-- a primary key violation, a foreign key violation, an unexpected internal error, a tablespace running out of space, a permission error, etc.-- because that makes it much more difficult for developers to handle the errors they can and propagate those they cannot. You'd have to do things like parse the text of the error string to figure out what went wrong and to figure out whether it's an error that you can handle or not. And heaven forbid that Oracle ever change the text of an error message or your parser causes you to misinterpret an error message. Similarly, it would generally be annoying if your code threw the same error code for every possible problem that was encountered.
If you are going to use a custom error code, it should communicate something above and beyond what the Oracle error code provides. It makes no sense, for example, to have a when others that converts the nice, usable Oracle error message and error stack to a pointless user-defined error. It may make perfect sense, though, for an ORA-20001 error to indicate that a foo already exists and an ORA-20002 error to indicate that a bar already exists when you have an application that deals with lots of foo's and bar's and those errors would make more sense to a user than a generic duplicate key error.
Just because you can do something doesn't mean that you should do something. Personally, as a consultant that comes in to mop up other people's messes, we (hired guns) hate developers that don't provide useful error messages in their code. Oracle does a pretty good job - most of the time. In the case of DUPS, it would be nice if in the error messaging, the developers added one more line to their error message and that would be the value being duplicated - what a concept. Maybe you don't send the text back to an end user, but should consider using dbms_system.ksdwrt - a proc that writes these errors to the alert log.
1) Can we use same error code (eg -20000) only for different error
scenarios at multiple places in PLSQL code?
You can. It is however not always useful - read on.
2) If we can use same error code in all places, why do we have 1000
codes?
To distinguish between different error conditions - read on.
3) What is the best practice to use error codes in Raise Application
Error?
Define some in-house standard, and keep following it - read on.
So, what is the point for user defined errors? They are great to show business logic problems (e.g. a newly added customer seems to be 950 years old, or born in the future), while standard Oracle errors show storage logic problems (e.g. referring to an unknown customer).
Depending on your application model, you may have the business logic implemented outside of the database, or you may prefer leveraging the powers of PL/SQL to implement the business logic inside the database.
This however is not too useful:
EXCEPTION
When Dup_val_on_index
then
Raise_application_error(-20000,'Cannot Insert duplicates');
WHEN OTHERS
THEN
Raise_application_error(-20000,SQLCODE||'-'||SQLERRM);
end;
First, you may ask yourself, what do you do in case of an error? Ok, you can print a nice call stack to the face of the end user - which is totally useless. I would say you can isolate all possible problems into something what the user can fix (i.e. pick a different login name if you hit a unique constraint), and errors what the user can not fix (for example, referring to a non-existing user because of a programming mistake).
I would say it makes sense to distinguish between errors this way. If you feel this logical, then:
wrap all exceptions to a custom BusinessError with e.g. -20000 if the user can do something with that. Use the error message to provide a useful description of what to do (i.e. "please pick a different login name" instead of "cannot insert duplicates") - and then your application is already in the top 5%.
wrap all other exceptions as a technical error, say with -20001. In case of an unexpected technical error, your application can do one thing: log it, and show a nice "something went wrong but we're trying to handle it" message to the user. Very less users can read/or interested to read Oracle call stacks :)
I am not totally sold on the standard oracle error messages. For example, a simple constraint violation error can be quite cryptic. I suggest to explicitly name all constraints, and follow a good naming convention. In that case, you don't need to actually handle any built-in Oracle error - as they will go to the application code, which will log them and show "something went wrong" to the user as it is not a business error.
Limitations:
This solution works very well if you have the business logic implemented in the database in PL/SQL. Of course, if you prefer application server business logic, e.g. Hibernate, then you'll have the fun to handle each and every constraint violation on your own.
This solution may need extensions if you're building a multi-language application. Business errors will have meaningful, human-friendly messages, and that comes in one language. Either you may need to throw a message in multiple languages (and pick the right one on the GUI), or implement your own translation file on top of it.
An example with business errors (one internal error, the dup_val_on_index is wrapped to business error, since that is caused by a wrong user input)
procedure addUser(in_birthDate date, ) is
pls_integer age=sysdate-in_birthDate;
begin
if age>100 then
raise_application_error(-20000,'Check birth date, you''re too old!');
elsif age<0 then
raise_application_error(-20000,'Birth date can not be in the future.');
end if;
insert into....
commit;
exception
when dup_val_on_index then
raise_application_error(-20000,'Please pick a different login name!');
end;
You may consider creating a procedure for throwing a business error:
CREATE OR REPLACE
Procedure throw (in_message varchar2) is
begin
raise_application_error(-20999,in_message);
end;
Which makes your code more readable:
begin
if age>100 then
throw('Check birth date, you''re too old!');

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

Resources