Can I access a cursor from within a procedure by name? - oracle

The cursor name would be passed in as a varchar2, and the cursor itself exists in the same package as the procedure.
Given only the name (and not a cursor reference) is it possible to access the cursor and loop through it?
If this requires the use of an "execute immediate" or similar, that's not out of the question... (though it's not quite clear to me how that would work, I was under the impression anything it declares is out of scope once it completes).
Apologies in advance, this seems like it should be obvious to me but I'm coming up blank.

Thinking about this a bit, I think you're going about it the wrong way. I'd UNION ALL the results from each of the "cursors" together and then use the "cursor name" to eliminate all the unwanted rows (which the optimizer should optimize away) so that you only get the rows you want. So something like
CREATE OR REPLACE PROCEDURE DO_SOMETHING(pin_Cursor_name IN VARCHAR2)
IS
CURSOR csrFruits IS
SELECT UPPER(pin_Cursor_name) AS FRUIT_TYPE,
VARIETY_NAME,
COLOR,
SIZE,
TARTNESS_RATING,
NULL AS FUZZ_LENGTH,
ROOTSTOCK,
NULL AS PEEL_THICKNESS
FROM APPLES
WHERE pin_Cursor_name = 'apples'
UNION ALL
SELECT UPPER(pin_Cursor_name) AS FRUIT_TYPE,
VARIETY_NAME,
COLOR,
SIZE,
NULL AS TARTNESS_RATING,
FUZZ_LENGTH,
NULL AS ROOTSTOCK,
NULL AS PEEL_THICKNESS
FROM PEACHES
WHERE pin_Cursor_name = 'peaches'
UNION ALL
SELECT UPPER(pin_Cursor_name) AS FRUIT_TYPE,
VARIETY_NAME,
COLOR,
SIZE,
NULL AS TARTNESS_RATING,
NULL AS FUZZ_LENGTH,
NULL AS ROOTSTOCK,
PEEL_THICKNESS
FROM KUMQUATS
WHERE pin_Cursor_name = 'kumquats'
UNION ALL
SELECT UPPER(pin_Cursor_name) AS FRUIT_TYPE,
VARIETY_NAME,
'GREEN' AS COLOR,
SIZE,
NULL AS TARTNESS_RATING,
FUZZ_LENGTH,
ROOTSTOCK,
NULL AS PEEL_THICKNESS
FROM KIWIS
WHERE pin_Cursor_name = 'kiwis';
BEGIN
FOR aRow IN csrFruits LOOP
DBMS_OUTPUT.PUT_LINE(pin_Cursor_name || ' - ' ||
aRow.VARIETY_NAME || ', ' ||
aRow.COLOR || ', ' ||
aRow.SIZE);
END LOOP;
END DO_SOMETHING;
So here we have a cursor which will read from one of four different tables (APPLES, PEACHES, KUMQUATS, and KIWIS) depending on the input parameter. The idea is to have each of the subqueries return a rowset of the same "shape", adding NULL AS XXX for each column which an individual subquery doesn't supply.
Best of luck.

Your original problem statement is quite a bit vague and it's unclear for me what constraints you have and what that "other system" expects as a return value. You also might have an XY-problem, so the answer by #bobjarvis might have a valid point too.
The key problem here is that in PL/SQL there is no way to convert an explicit cursor to a cursor variable. Thus the following "simple" solution is not possible:
-- pseudo PL/SQL code
cursor cur_a ...
cursor cur_b ...
function get_cursor(p_cur_name varchar2) return sys_refcursor is
v_cur sys_refcursor;
begin
execute immediate 'open v_cur for p_cur_name';
return v_cur;
end;
v_cur := get_cursor('cur_b');
In the example package below I assume that all the cursors will have the same result set structure. I was lazy and used weak cursor variables even I should have used strong ones. The package code should be easy to follow.
There is at least one other variation that might be useful for you - bulk collect the data to a collection and process the collection with other subroutines. Below print(varchar2) just demonstrates how to open-iterate-close a cursor "dynamically" with a dbms_output.put_line.
create or replace package so48 is
cursor cur_a is
select 'A1' as val, 1 as id from dual union all
select 'A2' as val, 2 as id from dual union all
select 'A3' as val, 3 as id from dual
;
cursor cur_b is
select 'B1' as val, 4 as id from dual union all
select 'B2' as val, 5 as id from dual union all
select 'B3' as val, 6 as id from dual
;
function get_cursor(p_cur_name in varchar2) return sys_refcursor;
procedure print(p_cur in sys_refcursor);
procedure print(p_cur_name in varchar2);
end;
/
show errors
create or replace package body so48 is
function get_cursor(p_cur_name in varchar2) return sys_refcursor is
v_cur sys_refcursor;
begin
case
when p_cur_name = 'A' then
open v_cur for
select 'A1' as val, 1 as id from dual union all
select 'A2' as val, 2 as id from dual union all
select 'A3' as val, 3 as id from dual
;
when p_cur_name = 'B' then
open v_cur for
select 'B1' as val, 4 as id from dual union all
select 'B2' as val, 5 as id from dual union all
select 'B3' as val, 6 as id from dual
;
else
null;
end case;
return v_cur;
end;
procedure print(p_cur in sys_refcursor) is
v_val varchar2(32767);
v_id number;
begin
loop
fetch p_cur into v_val, v_id;
exit when p_cur%notfound;
dbms_output.put_line('(val = ' || v_val || ')(id = ' || v_id || ')');
end loop;
end;
procedure print(p_cur_name in varchar2) is
plsql_compilation_error exception;
pragma exception_init(plsql_compilation_error, -6550);
v_cur_name constant varchar2(32767) := 'so48.' || p_cur_name;
v_plsql constant varchar2(32767) :=
q'[declare
v_val varchar2(32767);
v_id number;
begin
open ]' || v_cur_name || q'[;
loop
fetch ]' || v_cur_name || q'[ into v_val, v_id;
exit when ]' || v_cur_name || q'[%notfound;
dbms_output.put_line('(val = ' || v_val || ')(id = ' || v_id || ')');
end loop;
close ]' || v_cur_name || q'[;
end;]';
begin
execute immediate v_plsql;
exception
when plsql_compilation_error then
dbms_output.put_line('PL/SQL compilation error');
end;
end;
/
show errors
Example run
SQL> exec so48.print(so48.get_cursor('A'))
(val = A1)(id = 1)
(val = A2)(id = 2)
(val = A3)(id = 3)
PL/SQL procedure successfully completed.
SQL> exec so48.print('cur_b')
(val = B1)(id = 4)
(val = B2)(id = 5)
(val = B3)(id = 6)
PL/SQL procedure successfully completed.
SQL>

Related

Oracle generic function to read different cursor

Is it possible to fetch cursor data into array or table with dynamic cursor parameters ?
Example : We have 3 function fct1(), fct2(), fct3(). They all return a CURSOR with different data (from different table) and their size is between 20 and 100.
I'd like to call this 3 function with a generic function, fill a VARCHAR2 array and for example print this array.
I've found out how to fetch cursor into a VARRAY or VARCHAR2 but then you need to specify which cursor value you add into the array.
CREATE OR REPLACE PACKAGE BODY CURSOR_EXAMPLE AS
FUNCTION fct1 (param01 IN VARCHAR2) RETURN SYS_REFCURSOR AS
my_cursor SYS_REFCURSOR;
BEGIN
OPEN my_cursor FOR
SELECT a, b, c FROM table1, table2, table3;
RETURN my_cursor;
END fct1;
FUNCTION fct2 (param01 IN VARCHAR2) RETURN SYS_REFCURSOR AS
my_cursor SYS_REFCURSOR;
BEGIN
OPEN my_cursor FOR
SELECT q, w, e, r, t, y FROM table4, table5;
RETURN my_cursor;
END fct2;
FUNCTION fct3 (param01 IN VARCHAR2, param02 IN VARCHAR2, param03 IN VARCHAR2) RETURN SYS_REFCURSOR AS
my_cursor SYS_REFCURSOR;
BEGIN
OPEN my_cursor FOR
SELECT x, y, z FROM table6, table7, table8, table8;
RETURN my_cursor;
END fct3;
PROCEDURE generic_function (fct IN NUMBER, param01 IN VARCHAR2, param02 IN VARCHAR2, param03 IN VARCHAR2, cursor_out OUT SYS_REFCURSOR)
AS
BEGIN
IF fct = 1
cursor_out := fct1(param01);
ELSE IF fct = 2
cursor_out := fct2(param01);
ELSE IF
cursor_out := fct3(param01, param02, param03);
END IF;
LOOP
FETCH cursor_out INTO
-- HERE DEPENDING ON fct1, 2 and 3 add cursor_out variable into a VARRAY (extend it as needed)
EXIT WHEN cursor_out%NOTFOUND;
END LOOP;
-- PRINT VARRAY
END generic_function;
END CURSOR_EXAMPLE;
It appears that you are trying to devise a dynamic means of discovering what columns are contained within the cursor.
There is a way, but it involves using DBMS_SQL and once you convert the cursor to a DBMS_SQL cursor, you will have to use DBMS_SQL all the way as you process the records.
Here is an example of a test function that can return one of four different cursors, without static defined types:
CREATE OR REPLACE FUNCTION get_test_cursor (n in number default 1) return SYS_REFCURSOR
IS
c SYS_REFCURSOR;
BEGIN
IF n = 1
THEN
OPEN c FOR SELECT 'A' f1, 'B' f2 FROM DUAL;
END IF;
IF n = 2
THEN
OPEN c FOR SELECT cast(1.234 as number(6,2)) n1, cast(12 as number(8)) n2, 'C' f3 FROM DUAL;
END IF;
IF n = 3
THEN
OPEN c FOR select * from all_objects;
END IF;
if n = 4
then
OPEN c FOR select sysdate as current_dt from dual;
end if;
return c;
END;
/
Now, we can use the DBMS_SQL package to first convert the cursor from a REF CURSOR to a cursor number using dbms_sql.to_cursor_number.
Once we do this, we can use the rest of the DBMS_SQL API to inspect the cursor and do work on its data.
declare
cur sys_refcursor;
c number;
col_cnt INTEGER;
rec_tab DBMS_SQL.DESC_TAB;
--
-- This is a crude helper procedure to display one line of "DESCRIBE" output
--
PROCEDURE print_rec(rec in DBMS_SQL.DESC_REC) IS
BEGIN
DBMS_OUTPUT.PUT_LINE(rpad(rec.col_name, 41) || ' ' ||
rpad(case when rec.col_null_ok then ' ' else 'NOT NULL' end, 8) || ' ' ||
case when rec.col_type = DBMS_TYPES.TYPECODE_VARCHAR or rec.col_type = DBMS_TYPES.TYPECODE_VARCHAR2 then
'VARCHAR2(' || rec.col_max_len || ')'
when rec.col_type = DBMS_TYPES.TYPECODE_CHAR then
'CHAR(' || rec.col_max_len || ')'
when rec.col_type = DBMS_TYPES.TYPECODE_NUMBER then
case when rec.col_precision = 0 and rec.col_scale = -127 then 'NUMBER'
when rec.col_scale = 0 then 'NUMBER('||rec.col_precision||')'
else 'NUMBER(' || rec.col_precision || ', ' || rec.col_scale || ')'
end
when rec.col_type = DBMS_TYPES.TYPECODE_DATE then
'DATE'
else
'UNKNOWN'
end);
END;
begin
cur := test_cursor(3);
--
-- Convert the REF_CUR to a cursor number
--
c := dbms_sql.to_cursor_number(cur);
--
-- Use an API call to describe the columns
--
DBMS_SQL.DESCRIBE_COLUMNS(c, col_cnt, rec_tab);
--
-- Now loop through the columns and show them
--
dbms_output.put_line('Name Null? Type');
dbms_output.put_line('----------------------------------------- -------- ----------------------------');
for j in 1..col_cnt loop
print_rec(rec_tab(j));
end loop;
--
-- We can do other things at this point.
-- When done, close the cursor.
--
DBMS_SQL.CLOSE_CURSOR(c);
end;
/
Here is the output when we pass in 3, which queries the ALL_OBJECTS view:
Name Null? Type
----------------------------------------- -------- ----------------------------
OWNER NOT NULL VARCHAR2(128)
OBJECT_NAME NOT NULL VARCHAR2(128)
SUBOBJECT_NAME VARCHAR2(128)
OBJECT_ID NOT NULL NUMBER
DATA_OBJECT_ID NUMBER
OBJECT_TYPE VARCHAR2(23)
CREATED NOT NULL DATE
LAST_DDL_TIME NOT NULL DATE
TIMESTAMP VARCHAR2(19)
STATUS VARCHAR2(7)
TEMPORARY VARCHAR2(1)
GENERATED VARCHAR2(1)
SECONDARY VARCHAR2(1)
NAMESPACE NOT NULL NUMBER
EDITION_NAME VARCHAR2(128)
SHARING VARCHAR2(13)
EDITIONABLE VARCHAR2(1)
ORACLE_MAINTAINED VARCHAR2(1)
Once you are able to see what the columns of the cursor are, you are on your way to solving the problem of creating a dynamic procedure that consumes random cursors and populates arrays.

Execute query stored in variable and read result in Oracle

I have a procedure which returns result set from query which is dynamically generated in oracle. It do returns the result set but what I want is to process from information from the generated result set and save it in a table.
This is my query..
PROCEDURE GetItem(pitem_list in varchar2,
PGetData OUT SYS_REFCURSOR)
is
strsql2 long;
BEGIN
strsql2 :='SELECT val, val1, val2 from table1'; ----- This is a sample query as the main query is complex so just for simplicity I wrote this here.
open PGetData for strsql2; ----- This do returns me the result set generated from the query;
Now what I want is to execute the query stored in "strsql2" variable and read the result and process some information..
I thought of executing it from FOR LOOP.
strsql2 :='SELECT val, val1, val2 from table1';
for log in (select strsql2 from dual) ---- I even tried execute immediate
loop
if(log.val = 'TEST')
then
insert into table ----
else
update table --
end if;
end loop;
open PGetData for strsql2; --- After saving the result in table then return the result set..
Where I m going wrong here, or is there any other way to do it?
I m stuck here.
In that case, you can simply fetch from the cursor into local variables. In this case, I know that my query returns three columns, one an integer, one a string, and one a date so I'm declaring three local variables to hold the results.
declare
l_sql varchar2(1000);
l_rc sys_refcursor;
l_integer pls_integer;
l_string varchar2(100);
l_date date;
begin
l_sql := q'{select 1 int, 'foo' str, date '2020-12-21' dt from dual}';
open l_rc for l_sql;
loop
fetch l_rc
into l_integer, l_string, l_date;
exit when l_rc%notfound;
dbms_output.put_line( 'l_string = ' || l_string ||
' l_integer = ' || l_integer ||
' l_date = ' || to_char( l_date, 'dd-mon-yyyy' ) );
end loop;
end;
/
A liveSQL link

Stored procedure calling 2 values in parameter declaration

I have a stored procedure written like this:
CREATE OR REPLACE PROCEDURE Proc_location_example(in_data_ID IN VARCHAR2,
in_Location_name IN VARCHAR2)
IS
v_Location_ID NUMBER;
v_data_id NUMBER;
BEGIN
---------------------------------------------------------------------------
lv_prog_name := 'PRoc_location_example';
ln_step := 1;
SELECT Location_ID
INTO v_Location_ID
FROM random.client
WHERE Location_name = in_Location_name;
.....
END;
This procedure is getting 'NY ' passed in as an example for in_location_name. I want to pass in 'NY' and 'nj' to the location_name.
In other words the location name supports 2 name so which will be easiest way to do it?
You can add an expression with an IN operator such as
.. WHERE Location_name = in_Location_name AND in_Location_name IN ('NY','nj')
if case-sensitivity doesn't matter, then use
.. WHERE Location_name = in_Location_name AND REGEXP_LIKE(in_Location_name, 'Ny|nJ','i')
in order to restrict the parameters to those two values.
There are two options that I can think of that would satisfy your question.
Option #1: More Parameters
By adding more parameters you can use those parameters in your query for location IDs. If you are going to potentially more than 2 locations and don't want to keep having to add parameters, Option #2 is probably a better choice.
CREATE OR REPLACE PROCEDURE Proc_location_example (in_location_name1 IN VARCHAR2 DEFAULT NULL,
in_location_name2 IN VARCHAR2 DEFAULT NULL)
IS
TYPE location_id_t IS TABLE OF NUMBER;
v_location_ids location_id_t;
BEGIN
SELECT location_id
BULK COLLECT INTO v_location_ids
FROM (SELECT 101 AS location_id, 'NJ' AS location_name FROM DUAL
UNION ALL
SELECT 102, 'NY' FROM DUAL
UNION ALL
SELECT 103, 'MA' FROM DUAL)
WHERE location_name IN (in_location_name1, in_location_name2);
FOR i IN 1 .. v_location_ids.COUNT
LOOP
DBMS_OUTPUT.put_line ('ID #' || i || ': ' || v_location_ids (i));
END LOOP;
END;
/
BEGIN
Proc_location_example (in_location_name1 => 'NJ', in_location_name2 => 'NY');
END;
/
Option #2: Table Type Parameter
By using a table as your parameter, you can pass an unlimited number of "location names". This does require you to use a pre-defined table type, but this solution is more flexible in the number of "parameters"
CREATE OR REPLACE TYPE location_name_t IS TABLE OF VARCHAR2 (2);
CREATE OR REPLACE PROCEDURE Proc_location_example (in_locations location_name_t)
IS
TYPE location_id_t IS TABLE OF NUMBER;
v_location_ids location_id_t;
BEGIN
SELECT location_id
BULK COLLECT INTO v_location_ids
FROM (SELECT 101 AS location_id, 'NJ' AS location_name FROM DUAL
UNION ALL
SELECT 102, 'NY' FROM DUAL
UNION ALL
SELECT 103, 'MA' FROM DUAL)
WHERE location_name IN (select * from table(in_locations));
FOR i IN 1 .. v_location_ids.COUNT
LOOP
DBMS_OUTPUT.put_line ('ID #' || i || ': ' || v_location_ids (i));
END LOOP;
END;
/
BEGIN
Proc_location_example (in_locations => location_name_t('NJ','NY'));
END;
/

How to insert multiple row result of dynamic sql to another Table?

I write one dynamic SQL which the result of it is a table with 2 columns and multiple rows, I want to insert it to another table with 4 columns that 2 of them will be filled by the result of dynamic SQL, I try to use collection but don't know how to insert result to another table
CREATE OR REPLACE PROCEDURE P_C_SM_Failure_error_Code_P2P AS
v_month VARCHAR2(16); -- to get Month for each table
v_day VARCHAR2(16); -- to get day for each table
v_ERRCODE t_c_rpt_resultmsg.code%TYPE;
v_ERRMSG t_c_rpt_resultmsg.MESSAGE%TYPE;
v_param VARCHAR2(16);
v_sql VARCHAR2(3000);
v_result number;
type t_c_result is record (Err_code varchar2(2000), Err_count number);
type v_t_result is table of t_c_result index by PLS_INTEGER;
v_t1_result v_t_result;
BEGIN
v_sql :='0';
v_param := 'Gateway_G';
v_result := '0';
select to_char(sysdate - 1,'MM') into v_month from dual;
select to_char(sysdate - 1,'DD') into v_day from dual;
-- Get count of P2P
v_sql := '(select count(*), error_code from (
select error_code from sm_histable'||v_month||''||v_day||'#ORASMSC01 where
orgaccount = '''||v_param||''' and destaccount = '''||v_param||''' and
sm_status <> 1 union all
select error_code from sm_histable'||v_month||''||v_day||'#ORASMSC02 where
orgaccount = '''||v_param||''' and destaccount = '''||v_param||''' and
sm_status <> 1 )
group by error_code)';
EXECUTE IMMEDIATE v_sql bulk collect into v_t1_result;
--insert into t_c_rpt_result2 values (trunc(sysdate, 'DD'), v_errcount,
v_err_code,'Failure_error_Code_P2P');
--for indx in 1 .. v_t1_result.COUNT
--loop
--dbms_output.put_line (v_t1_result (indx).Err_code);
--end loop;
You may append the constant values of date and the error message to the subquery and run a dynamic insert. It should also work if you remove the outer parentheses of your dynamic sql since constants can be included in group by. Always remember to pass values as bind variables rather than concatenating them (v_param). Also, specify the column names explicitly in an INSERT statement.
v_sql := '(select count(*) as cnt, error_code
from (
select error_code from sm_histable'||v_month||''||v_day||'#ORASMSC01
where orgaccount = :x and destaccount = :x and sm_status <> 1
union all
select error_code from sm_histable'||v_month||''||v_day||'#ORASMSC02
where orgaccount = :x and destaccount = :x and sm_status <> 1 )
group by error_code)';
EXECUTE IMMEDIATE v_sql bulk collect into v_t1_result using v_param;
EXECUTE IMMEDIATE 'insert into t_c_rpt_result2(err_dt,err_msg,errcount,error_code)
select :dt,:msg,cnt,error_code from '|| v_sql
USING trunc(sysdate, 'DD'),'Failure_error_Code_P2P',v_param;
I think you are looking at an excellent use case for FORALL. The collection you are populating needs to be done with execute immediate since you are dynamically constructing the table name. But the insert into t_c_rpt_result2 looks static to me.
BEGIN
v_sql :=
'(select count(*) as cnt, error_code
from (
select error_code from sm_histable'
|| v_month
|| ''
|| v_day
|| '#ORASMSC01
where orgaccount = :x and destaccount = :x and sm_status <> 1
union all
select error_code from sm_histable'
|| v_month
|| ''
|| v_day
|| '#ORASMSC02
where orgaccount = :x and destaccount = :x and sm_status <> 1 )
group by error_code)';
EXECUTE IMMEDIATE v_sql BULK COLLECT INTO v_t1_result USING v_param;
FORALL indx IN 1 .. v_t1_result.COUNT
INSERT INTO t_c_rpt_result2 (err_dt,
err_msg,
errcount,
ERROR_CODE)
VALUES (TRUNC (SYSDATE, 'DD'),
'Failure_error_Code_P2P',
v_t1_result (indx).cnt,
v_t1_result (indx).ERROR_CODE);
END;
Find more examples of FORALL on LiveSQL here. Of course, even if your insert was dynamic, you can use FORALL - put the execute immediate directly "inside" the FORALL statement. But I don't think that complexity is justified here.
Hope that helps!

How to get name of caller object in oracle

In Oracle, it's complicated to properly get name of caller object when it is wrapped in a package. Here is my attempt to achieve this:
CREATE OR REPLACE function GET_MY_NAME return varchar2 is
v_owner varchar2(50);
v_name varchar2(50);
v_lineno number ;
v_caller_t varchar2(50);
begin
-- let's determine the object
owa_util.who_called_me(
v_owner
, v_name
, v_lineno
, v_caller_t
);
dbms_output.put_line(v_caller_t || ' ' || v_owner || '.' || v_name || ' at ' || v_lineno);
with s as (
select s.*
, max(line - 1) over (partition by name order by line rows between 1 following and 1 following) to_line
, regexp_substr(s.text, '\w+', 1, 2) func_name
from all_source s
where s.type = v_caller_t
and s.owner = v_owner
and s.name = v_name
and regexp_like(s.text, 'procedure|function')
)
select max(nvl(s.func_name, v_name ))
, max(nvl(s.name , v_owner))
into v_name, v_owner
from s
where v_lineno between s.line and nvl(s.to_line, 9999999);
return v_owner || '.' || v_name;
end;
The idea is to determine lines within the source of each inner object is located and take corresponding object. This works fine for packages, but for functions and procedures it cannot determine if it was called from the main block. Here is a little demo:
create or replace function check_my_name return varchar2 is
function sub_check_my_name return varchar2 is
begin
return get_my_name;
end;
begin
dbms_output.put_line(sub_check_my_name);
return get_my_name;
end;
/
You can see that sub_check_my_name works, but direct get_my_name call also returns sub_check_my_name. How can I determine if the call occures inside main function block?
I did something as simple as this,
Declared a variable as below,
call_stack VARCHAR2(3000) := '';
Now use the below to get the levels of the calls,
FOR j IN REVERSE 1..utl_call_stack.dynamic_depth() LOOP
call_stack := CASE
WHEN call_stack IS NOT NULL THEN
call_stack || '->'
END
|| nvl(utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(j)), '');
END LOOP;
dbms_output.put_line(call_stack);
This gives the call hierarchy in the call_stack separated by '->'

Resources