How to correctly call a SQL stored procedure using R's DBI library? - sql

Below I have a stored procedure created in Oracle. I've cut out the majority of the middle and am just focusing on the input and output:
CREATE OR replace PROCEDURE datatrans_n.dc_cpt_patient_new
-- Declaring input and output.
( p_study_id IN patient.study_id%TYPE,
p_subject IN patient.subject%TYPE,
p_subject_dict IN patient.subject_dict%TYPE,
p_site IN patient.site%TYPE,
p_cancer_type IN patient.cancer_type%TYPE,
p_comments IN patient.comments%TYPE,
ps_patient_status_name IN patient_status.patient_status%TYPE,
pml_link_date IN NVARCHAR2,
status OUT NUMBER ) ...
-- Returning 1 if successful.
status := SQL%rowcount;
-- Committing.
COMMIT;
ELSE
-- Returning 0 if unsuccessful.
status := 0;
END IF;
-- Raising exceptions.
EXCEPTION
WHEN no_data_found THEN
NULL;
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END dc_cpt_patient_new;
/
And I'm attempting to call this stored procedure in R using the DBI library like so:
# Creating SQL.
query <- DBI::sqlInterpolate(
conn = con,
sql = "DECLARE STATUS NUMBER;
BEGIN
DATATRANS_N.DC_CPT_PATIENT_NEW(?study_id, ?subject, ?subject_dict, ?site,
?cancer_type, ?comments, ?patient_status,
?icf_date, STATUS);
END;",
.dots = hold_patient[c("study_id", "subject", "subject_dict", "site",
"cancer_type", "comments", "patient_status", "icf_date")]
)
# Executing SQL.
return <- DBI::dbExecute(con, query)
The issue is that when executing my SQL query, I'm not correctly capturing the value of STATUS in my stored procedure in my R variable return. I've tested this by editing the line Status := SQL%ROWCOUNT; to Status := 3; --SQL%ROWCOUNT;, and both times return has a value of 1.
How can I call this stored procedure so that I can capture the value of STATUS in return?

Related

Cannot rollback while a subtransaction is active - Error 2D000

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

How to find what data caused Oracle function failure?

sorry if this not the right place.
I am doing a SQL SELECT statement, invoking a function. It's a large data dump - about 10,000 records.
I am calling a function to preform some calculations, but its failing.
One ore more of those records has bad data that is causing the function to crash.
Is there any way to see exactly what data caused the crash readily Or should I create some code to run the function by hand for each of 10,000 records? I could create code that generates the input data fairly straightforwardly, then run the function like this SELECT MY_FUNCT(1,1,1) FROM DUAL; but I am wondering if there is a better way.
For reference I am running the SQL query like this.
SELECT
MY_FUNCT(A.FOO, A.BAR)
FROM TABLE A
WHERE ....;
As others have said, you just need to handle the error and not raise it all the way. A neat way of doing this would be to create a wrapper function for your function that sometimes fails, you can declare this function within your select query using a with pl/sql clause:
Let's say this is your function that sometimes fails
create or replace function my_funct (inputnumber number)
return varchar2
is
sString varchar2(200);
begin
if inputnumber = 42 then
raise_application_error(-20001,'UH OH!');
end if;
sString := 'Input: '||inputnumber;
return sString;
end my_funct;
/
We can define a function that takes the same inputs, and just calls this function, then we just need to add some error handling (obviously never just rely on dbms_output to capture errors, this is just to make it obvious):
function my_funct_handle (inputnumber number)
return varchar2
is
begin
return my_funct (inputnumber => inputnumber);
exception when others then
dbms_output.put_line(sqlerrm||' at '||inputnumber);
return 'ERROR';
end;
And we can then just stick that in our query using with function
with
function my_funct_handler (inputnumber number)
return varchar2
is
begin
return my_funct (inputnumber => inputnumber);
exception when others then
dbms_output.put_line(sqlerrm||' at '||inputnumber);
return 'ERROR';
end;
select my_funct_handler (id), string_col
from as_table;
/
I get both the dbms_output text to describe the error and the ID but I could also filter on the results of that function to only show me the erroring rows:
with
function my_funct_handle (inputnumber number)
return varchar2
is
begin
return my_funct (inputnumber => inputnumber);
exception when others then
dbms_output.put_line(sqlerrm||' at '||inputnumber);
return 'ERROR';
end;
select my_funct_handle (id), string_col
from as_table
where my_funct_handle (id) = 'ERROR';
/
MY_FUNCT_HANDLE(ID)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
STRI
----
ERROR
blah
ORA-20001: UH OH! at 42
ORA-20001: UH OH! at 42
(I get two errors shown in the dbms_output output as the function can be called multiple times - once as part of the select list and once as part of the where clause.)
One option is to handle the exception properly, e.g.
create or replace function my_funct(par_foo in number, par_bar in number)
return number
is
retval number;
begin
select sal
into retval
from emp
where ename = par_foo
and deptno = par_bar;
return par_foo/par_bar;
exception --> this
when no_data_found then
return null;
end;
If you want, you can even log those errors. How? Make the function autonomous transaction (so that it could write into the log table; you'll have to commit that insert). Store all relevant information (including SQLERRM). Once your code finishes, check what's written in the log file and then decide what to do.
Or, you could even continue current task by enclosing that select into its own begin-exception-end block within a loop, e.g.
begin
for cur_r in (select ... from ...) loop
begin
-- your current SELECT
SELECT
MY_FUNCT(A.FOO, A.BAR)
FROM TABLE A
WHERE ....;
exception
when others then
dbms_output.put_line(cur_r.some_value ||': '|| sqlerrm);
end;
end loop;
end;
one better approach is to create a error handler Package/Procedure which will write it into a table and call it from the function, this way all the errors will be captured in a Oracle table.
--- untested -- You can create other columns to capture the function name, date, and other details in the error table.
PROCEDURE SP_ERROR_INS_ERR_COMMON (n_ERR_CODE NUMBER, c_ERR_SOURCE VARCHAR2, n_ERR_LINE NUMBER, c_ERR_DESC VARCHAR2, C_USER_COMMENT VARCHAR2 ) IS
n_Log_Id NUMBER;
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
n_Log_Id := 0;
INSERT INTO ERROR_LOG_COMMON
(ERROR_CODE, ERROR_SOURCE, ERROR_LINE, ERROR_DESCRIPTION, USER_COMMENT)
VALUES
(n_ERR_CODE, c_ERR_SOURCE, n_ERR_LINE, c_ERR_DESC, C_USER_COMMENT
);
COMMIT;
raise_application_error( -20001,SUBSTR(SQLERRM, 1, 200));
END SP_ERROR_INS_ERR_COMMON;
In your function you can call the error
EXCEPTION
WHEN OTHERS
THEN
vn_errcode := SQLCODE;
vc_errmsg := SUBSTR (SQLERRM, 1, 4000);
sp_error_ins_err_common(vn_ErrCode,
'SP_RP_DIM_COMPARE', NULL, vc_ErrMsg, substr('batch_id ' || g_BATCH_ID ||
l_str_err_msg,1,4000) );
RAISE;

Identify when a function is executed in a SQL Query or in a PL/SQL procedure

Is there any way to identify when a pl/sql function is executed in SQL Query and when is executed in a procedure or PL/SQL anonymous block? (I don't want to pass any parameter for manual identification)
The main reason I need that is when a function is executed in a SQL query I wouldn't like to raise an exception in case of failure, I would be satisfied just with a returned value NULL. But the same function when is executed in pl/sql script I want to raise exception.
Thank you in advance.
Why don't you add a parameter to the function to indicate whether or not to throw an exception/return null? When you call the function you can choose the behaviour you need.
create or replace function do_something(p_parameter1 < some_type >
,p_raise_exception varchar2 default 'Y') return < sometype > is
begin
--.. calculating .. .
return result;
exception
when others then
if p_raise_exception is 'Y'
then
raise;
else
return null;
end if;
end;
Alternatively owa_util seems to provide some functionality you can use.
create or replace function do_something(p_parameter1 < some_type >) return < sometype > is
l_owner varchar2(100);
l_name varchar2(100);
l_lineno number;
l_caller_t varchar2(100);
begin
--.. calculating .. .
return result;
exception
when others then
owa_util.who_called_me(l_owner, l_name, l_lineno, l_caller_t)
-- who called me result seems empty when called from sql.
if l_owner is not null
then
raise;
else
return null;
end if;
end;
Of course :
Hiding all errors is bad practise
Well, looking around I found that there is a hack available:
The exception NO_DATA_FOUND isn't propagated when you call PL/SQL in SQL. So you can use this to "return null" instead of get an exception when calling it from SQL:
create or replace function f
return int as
begin
raise no_data_found;
return 1;
end f;
/
select f from dual;
F
null;
declare
v integer;
begin
v := f;
end;
Error report -
ORA-01403: no data found

Checking execution of procedure (PL/SQL, Oracle)

I have below PL/SQL block:
Begin
insert1();
insert2();
insert3();
procedureTest();
End
Each insertTest is making insert to some tables.
Now, if one of this procedure will not insert anything, my procedureTest should not execute.
How to do that?
you have two possibilities to check if the three procedures insert rows:
1) launch a query or call a function to select the count of rows expected to be inserted after each call;
2) store the result of the command sql%rowcount: this command returns the count of rows affected from preceeding insert/update/delete instruction.
Note that, for the 2nd option, if insert1/2/3 insert rows using only one single insert instruction AND this single insert is the last statement of the procedure, then you can call sql%rowcount after the call to insert1/2/3 in the following way:
declare
ret NUMBER := 0;
Begin
insert1();
ret := sql%rowcount; -- counts rows inserted in insert1
insert2();
ret := ret + sql%rowcount; -- counts rows inserted in insert1 + insert2
insert3();
ret := ret + sql%rowcount; -- counts rows inserted in insert1 + insert2 + insert3
if ret > 0 then
procedureTest();
end if;
End
Instead, if the three procedures use more than one insert statement each OR perform a select or any other instruction after the insert, you have to check sql%rowcount inside insert1/2/3 immediately after each insert statement and return the sum of these results as output parameter (if insert1/2/3 are procedures) or as return value (if insert1/2/3 are functions).
Hope it helps.
From this
if one of this procedure will not insert anything
I understand you want to stop execution of the rest of the code. So, for this, a suggestion would be: in those functions insert1/2/3, return the number of rows affected with:
sql%rowcount
Notice that this function counts the number of rows affected, so to speak, it counts only with insert, update, and delete statements (not select), for select, you'd need a cursor, more information you can find here: https://asktom.oracle.com/pls/apex/asktom.search?tag=rows-affected
Then, as those functions return a value that, if have inserted anything will return a number > 0, then you can store then in a variable and check their values, if any of then is == 0, then return, otherwise, execute procedureTest(). To interrupt execution, you can use return;
One approach would be to have your insert1/2/3 procedures return the number of rows inserted as OUT parameters, which you could then capture and examine in the calling procedure.
declare
l_rowcount1 integer := 0;
l_rowcount2 integer := 0;
l_rowcount3 integer := 0;
l_rowcount_total integer := 0;
procedure insert1
( p_rowcount out integer )
is
begin
insert into sometable(a, b, c)
select x, y, z from somewhere
where whatever = somethingelse;
p_rowcount := sql%rowcount;
end insert1;
begin
insert1(l_rowcount1);
insert1(l_rowcount2);
insert1(l_rowcount3);
l_rowcount_total := l_rowcount1 + l_rowcount2 + l_rowcount3;
if l_rowcount_total > 0 then
do_something();
end if;
end;
create or replace procedure insert1 is
o_error varchar2(150);
begin
-- your statement 1
exception when others then rollback; o_error := ' an error occured! '; raise_application_error(-20101,o_error);
end;
Create procedures insert1 - 2 - 3 as preceding by iterating error code by one in every consecutive insert procedure as -20002 & -20003, and statements 2 & 3.
And call as you did before with a returning error argument:
Begin
insert1;
insert2;
insert3;
procedureTest();
End
If any error raises, you'll see it as an alert and your program unit will stop running.

how to make variable hold the value taken from one procedure to be used in another procedure in plsql

I have a package with two procedures
in same package.
p_set_values (pn_id_number IN number)
p_get_values (prc_records OUT sys_refcursor)
p_set_values (pn_id_number IN number)
inserts data in to my_table where id_number = pn_id_number
p_get_values (prc_records OUT sys_refcursor) - this procedure has to select the value from my_table where id_number = pn_id_number (Note: same id number which is used to insert the value ,now used to set the values inserted.)
I have declared package level variable and assigned as ln_id_number = pn_id_number.
Now when using this 'ln_id_number' in second procedure, the value of ln_id_number is nothing when checked using dbms_putput.putline(ln_id_number)
Please help me to do this,
Procedures are as follows
CREATE OR REPLACE PACKAGE BODY pck_exit_info
IS
ln_id_number po010.polref%TYPE;
PROCEDURE p_set_values(pn_id_number IN number
,pv_exit_option IN VARCHAR2)
IS
ln_system_date cs340.sysdte%TYPE;
lv_audaplcde package_audits.audit_application_code%TYPE;
ln_audstfno package_audits.audit_staff_number%TYPE;
ln_audupdid NUMBER;
lv_exit_option VARCHAR2(1);
BEGIN
ln_system_date := pck_system_context.f_get_system_date;
ln_id_number := pn_id_number;
dbms_output.put_line(ln_id_number);
SELECT AUDUPDID_SEQ.NEXTVAL INTO ln_audupdid FROM DUAL;
IF pv_exit_option = 'LE' THEN
lv_exit_option := 'L';
ELSE
lv_exit_option := 'S';
END IF;
SELECT audit_application_code,audit_staff_number
INTO lv_audaplcde,ln_audstfno
FROM package_audits
WHERE process = 'PCK_LEAVERS'
AND subprocess ='DEFAULT';
INSERT INTO my_table
VALUES(pn_id_number
,ln_system_date
,lv_exit_option
,ln_audupdid
,ln_system_date
,lv_audaplcde
,ln_audstfno);
END set_employer_exit_info;
PROCEDURE p_get_values(prc_records OUT SYS_REFCURSOR) IS
BEGIN
/*dbms_output.put_line('ID inside get employer');
dbms_output.put_line(ln_id_number);*/
OPEN prc_records FOR
SELECT *
FROM my_table
WHERE polref = ln_id_number;
-- CLOSE prc_policy;
END get_employer_exit_info;
END pck_exit_info;