Oracle: Using an array datav type in package - sql

I have a package which contains a procedure in which I need to query a list of id numbers, which are varchar2. I will have to query this same list multiple times, and I'd rather not have redo the query. Ideally I'd like to make a function in my package that would return the list of id numbers. So I could load the array of id numbers into a variable, and then use that variable as a table throughout my procedure. I've been googling like crazy and it just doesn't seem like it's possible. Is there some way to do this?
Note: I'm not able to create a new type at the schema level, and it won't let me do this with a local collection type.
Also, I would prefer not to use dynamic sql; the main query in my procedure is enormous, and I don't want to deal with a string of that size.
I want to do something like this:
id_number_list array_type := my_function();
select *
from my_table mt
left join table(id_number_list) idl on mt.id_number = idl.column_value;
EDIT: Thanks for your help so far! MTO's answer works for a select statement join, like I described above. However, I also need to delete from the table where the id_number is in the list. This gives me an "invalid data type" error. What could explain this? The data type should always be the same: varchar2(10).
Here I create the type at package level:
type string_list is table of varchar2(10);
Then I create a function that returns the list of id numbers (for our purposes, the "action" is always c_action_refresh, so the if statement is true):
function get_modified_ids(scope in smallint, action in smallint) return string_list is
modified_ids string_list := string_list();
last_refreshed date;
begin
last_refreshed := get_last_refreshed_date(scope,action);
if action = c_action_refresh then
select id_number
bulk collect into modified_ids
from(
select id_number
from adv.hr_giving cg
join adv.pbi_dates d
on d.DATE_FULL = trunc(cg.processed_date)
where d.RELATIVE_DATE >= last_refreshed
and d.RELATIVE_DATE <= trunc(CURRENT_DATE)
and cg.fiscal_year >= adv.current_fiscal_year - 6
union
select gi.gift_donor_id as id_number
from adv.gift gi
where gi.date_added >= last_refreshed
or gi.date_modified >= last_refreshed
union
select p.pledge_donor_id as id_number
from adv.pledge_rev p
where p.date_added >= last_refreshed
or p.date_modified >= last_refreshed
union
select a.id_number
from adv.affiliation a
where a.date_added >= last_refreshed
or a.date_modified >= last_refreshed
);
end if;
return(modified_ids);
end get_modified_ids;
Then, in my procedure, I initialize a variable by calling the function:
modified_ids string_list := get_modified_ids(scope,action);
Then I try to use the list in a delete statement:
delete from advrpt.pbi_gvg_profile_ag p
where p.id_number in
(select column_value from table(modified_ids));
This gives the error ORA-00902: invalid datatype. The type of id_number is varchar2(10). And again, it works fine in a join in a select statement.
So why am I getting this error?

Don't use a variable in the package (as there would only be a single variable and if your procedure is called twice in short succession then the second set of values would overwrite the first and potentially cause issues if that happened mid-way through processing the first invocation).
Instead, create a user-defined collection type:
CREATE TYPE number_list IS TABLE OF NUMBER;
And pass a collection as an argument to the procedure:
CREATE PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN o_cursor1 FOR
SELECT *
FROM your_table
WHERE id MEMBER OF i_numbers;
OPEN o_cursor2 FOR
SELECT y.*
FROM your_table y
INNER JOIN TABLE(i_numbers) n
ON (y.id = n.COLUMN_VALUE);
END;
/
Then call it using, for example:
DECLARE
v_cur1 SYS_REFCURSOR;
v_cur2 SYS_REFCURSOR;
v_id your_table.id%TYPE;
v_value your_table.value%TYPE;
BEGIN
your_procedure(number_list(1,5,13), v_cur1, v_cur2);
LOOP
FETCH v_cur1 INTO v_id, v_value;
EXIT WHEN v_cur1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_id || ', ' || v_value);
END LOOP;
END;
/
Or create the type as part of your package:
CREATE PACKAGE your_package AS
TYPE number_list IS TABLE OF NUMBER;
PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
);
END;
/
Then create the package body:
CREATE PACKAGE BODY your_package AS
PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN o_cursor1 FOR
SELECT *
FROM your_table
WHERE id IN (SELECT COLUMN_VALUE FROM TABLE(i_numbers));
OPEN o_cursor2 FOR
SELECT y.*
FROM your_table y
INNER JOIN TABLE(i_numbers) n
ON (y.id = n.COLUMN_VALUE);
END;
END;
/
Note: The MEMBER OF operator only works with collections defined in the SQL scope and not collections defined locally in a PL/SQL scope.
Then call it using, for example:
DECLARE
v_cur1 SYS_REFCURSOR;
v_cur2 SYS_REFCURSOR;
v_id your_table.id%TYPE;
v_value your_table.value%TYPE;
BEGIN
your_package.your_procedure(your_package.number_list(1,5,13), v_cur1, v_cur2);
LOOP
FETCH v_cur1 INTO v_id, v_value;
EXIT WHEN v_cur1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_id || ', ' || v_value);
END LOOP;
END;
/
fiddle

To me, it looks as if you have it all. If not, here's an example.
Function:
SQL> create or replace function f_test
2 return sys.odcinumberlist
3 is
4 begin
5 return sys.odcinumberlist(10, 20);
6 end;
7 /
Function created.
How to use it?
SQL> set serveroutput on
SQL> declare
2 id_number_list sys.odcinumberlist := f_test;
3 begin
4 for cur_r in (select e.deptno, e.ename
5 from emp e join table(id_number_list) idl on idl.column_value = e.deptno
6 order by 1, 2
7 )
8 loop
9 dbms_output.put_line(cur_r.deptno ||' '|| cur_r.ename);
10 end loop;
11 end;
12 /
10 CLARK
10 KING
10 MILLER
20 ADAMS
20 FORD
20 JONES
20 SCOTT
20 SMITH
PL/SQL procedure successfully completed.
SQL>

Related

How to tokenize semicolon separated column value to pass to IF statement in a function in Oracle DB

I have a table called 'config' and when I query it in following manner:
SELECT value FROM config WHERE property = 'SPECIAL_STORE_ID'
its response will be: 59216;131205;76707;167206 //... (1)
I want to tokenize the above values using semicolon as the delimiter and then use them in a user-defined Function's IF statement to compare, something like this:
IF in_store_id exists in (<delimited response from (1) above>)//...(2)
THEN do some stuff
where in_store_id is the parameter passed-in to the function
Is this possible to do as one-liner in (2) above ?
I'm on Oracle 12c
One-liner? I don't think so, but - if you're satisfied with something like this, fine.
SQL> select * From config;
VALUE PROPERTY
-------------- ----------------
7369;7499;7521 SPECIAL_STORE_ID
SQL> declare
2 in_store_id varchar2(20) := 7369;
3 l_exists number;
4 begin
5 select instr(value, ';' || in_store_id || ';')
6 into l_exists
7 from config
8 where property = 'SPECIAL_STORE_ID';
9
10 if l_exists > 0 then
11 dbms_output.put_line('that STORE_ID exists in the value');
12 else
13 dbms_output.put_line('that STORE_ID does not exist in the value');
14 end if;
15 end;
16 /
that STORE_ID exists in the value
PL/SQL procedure successfully completed.
SQL>
If the delimited response is a collection then you can use member of to check if the collection contains the ID or not like
create or replace procedure test_procedure2(p_property in varchar2, p_id in varchar2) is
type test_t is table of varchar2(20);
l_ids test_t;
begin
select regexp_substr(value, '[^;]+', 1, level) bulk collect into l_ids
from (select value from config where property = p_property)
connect by level <= regexp_count(value, ';')+1;
if(p_id member of (l_ids)) then
dbms_output.put_line('Do stuff for '||p_property||' '||p_id);
end if;
end;
/
or do it without the collection with intermediate select like
create or replace procedure test_procedure1(p_property in varchar2, p_id in varchar2) is
l_flag number(3);
begin
select count(1) into l_flag from dual where p_id in (
select regexp_substr(value, '[^;]+', 1, level)
from (select value from config where property = p_property)
connect by level <= regexp_count(value, ';')+1
);
if(l_flag > 0) then
dbms_output.put_line('Do stuff for '||p_property||' '||p_id);
end if;
end;
/
See fiddle

Oracle PL/SQL - how to use table of varchar in select statement?

I have a table type declared as
TYPE t_table IS TABLE OF VARCHAR2(15) INDEX BY PLS_INTEGER;
I am trying to use it in a procedure with a select statement, but it does not work:
procedure get_something (
p_in_list IN t_table,
p_out_list OUT t_table
)
IS
BEGIN
SELECT item
BULK collect into p_out_list
from my_table
where myrow in (select * from table(p_in_list));
END get_something;
How can I use this in a select ... in statement?
This it tested in 19.0. I don't have earlier versions to test on right now, but I think it will require at least 12.1.
First, if you need the type to be an associative array ('index by'), it needs to be in a package specification:
create or replace package demo_pkg
as
type t_table is table of varchar2(15) index by pls_integer;
end demo_pkg;
Then SQL can see it:
declare
-- type t_table is table of varchar2(15) index by pls_integer;
subtype t_table is demo_pkg.t_table;
from_list t_table;
to_list t_table;
procedure get_something
( p_in_list in t_table
, p_out_list out t_table )
is
begin
select dummy bulk collect into p_out_list
from dual
where dummy in (select * from table(p_in_list));
end get_something;
begin
from_list(1) := 'X';
from_list(2) := 'Y';
from_list(3) := 'Z';
get_something(from_list, to_list);
end;
From 18c you can populate the array declaratively using a qualified expression, e.g:
from_list t_table := demo_pkg.t_table(1 => 'X', 2 => 'Y', 3 => 'Z');
or
get_something
( demo_pkg.t_table(1 => 'X', 2 => 'Y', 3 => 'Z')
, to_list );
Some of these restrictions are because associative arrays aren't really a natural fit for SQL queries, and support for them took a while to be added. If you declare t_table as a regular nested table, it should work in an earlier version:
create or replace package demo_pkg
as
type t_table is table of varchar2(15);
end demo_pkg;
or create it as a standalone SQL object:
create or replace type t_table as table of varchar2(15);
This also makes a member of construction possible:
declare
from_list t_table := t_table('X','Y','Z');
to_list t_table;
procedure get_something
( p_in_list in t_table
, p_out_list out t_table )
is
begin
select dummy bulk collect into p_out_list
from dual
where dummy member of p_in_list;
end get_something;
begin
get_something(from_list, to_list);
end;
member of only works with "nested table" collections, not associative arrays or varrays. I can never really see the point of varrays, unless the size limit is so useful for your business logic that you can live with all the lost functionality.
Here's one option; see if it helps.
Sample table and type:
SQL> create table my_table as
2 select dname item, loc myrow from dept;
Table created.
SQL> create or replace type t_table as varray(20) of varchar2(20);
2 /
Type created.
Procedure:
SQL> create or replace procedure
2 get_something (
3 p_in_list in t_table,
4 p_out_list out t_table
5 )
6 is
7 begin
8 select item
9 bulk collect into p_out_list
10 from my_table
11 where myrow in (select * from table(p_in_list));
12 end get_something;
13 /
Procedure created.
SQL>
Testing:
SQL> set serveroutput on
SQL> declare
2 l_in t_table;
3 l_out t_table;
4 begin
5 l_in := t_table();
6 l_in.extend(2);
7 l_in(1) := 'DALLAS';
8 l_in(2) := 'NEW YORK';
9
10 get_something(l_in, l_out);
11
12 for i in 1 .. l_out.count loop
13 dbms_output.put_line(l_out(i));
14 end loop;
15 end;
16 /
RESEARCH
ACCOUNTING
PL/SQL procedure successfully completed.
SQL>

oracle cursor parameter as collection

it works like:
create or replace procedure back_end_proc(p_rc OUT SYS_REFCURSOR) is
v_script VARCHAR2(4000); begin
v_script := 'select sysdate data from dual where ''a'' = :a union all select sysdate-1 data from dual where ''b'' = :b';
open p_rc for v_script using 'a','b'; end;
when calling the procedure:
DECLARE
rc SYS_REFCURSOR;
BEGIN
victom.back_end_proc2(:rc );
END;
will have an output rows:
DATA
27.06.2017 11:25:02
26.06.2017 11:25:02
THE QUESTION IS:
how to modify the using 'a','b' params into one array or some collection data that will contain 'a','b' as one string or smth like one single value?
If your target query looked like this ...
select * from whatever
where id in ( .... )
it would be quite simple to pass a collection.
create or replace type tt1 as table of varchar2(1);
/
create or replace procedure back_end_proc(
p_ids in tt1
, p_rc OUT SYS_REFCURSOR) is
begin
open p_rc for
'select * from whatever
where id member of :1'
using p_ids;
end;
However, your posted case is slightly more complicated because you want to assign specific values from the collection in each sub-query. This means you need to use a VARRAY because that is the only collection type which guarantees ordering. So:
create or replace type va1 as varray(10) of varchar2(1);
/
create or replace procedure back_end_proc(
p_ids in va1
, p_rc OUT SYS_REFCURSOR) is
begin
open p_rc for
'select sysdate from dual where ''a'' = :1
union all
select sysdate-1 from dual where ''b'' = :2'
using p_ids(1), p_ids(2);
end;

How to handle array list in a dynamic sql query

I want to create a dynamic query to handle array list.
create or replace TYPE p_type IS table of varchar2(4000) ;
CREATE OR REPLACE PROCEDURE test_proc_sk(
p_class_array IN p_type,
p_emp_record OUT SYS_REFCURSOR)
IS
lv_stmt VARCHAR2(100);
BEGIN
lv_stmt := 'Select * from dept where deptno = 10 ';
IF(p_class_array IS NOT NULL) THEN
lv_stmt := lv_stmt || 'AND dname IN (select column_value from table(' || p_class_array ||'))';
END IF;
dbms_output.put_line(lv_stmt);
OPEN p_emp_record FOR lv_stmt;
END;
It gives a compilation error
Error(9,5): PL/SQL: Statement ignored Error(9,23): PLS-00306: wrong
number or types of arguments in call to '||'
Please help
Use the MEMBER OF operator:
CREATE OR REPLACE PROCEDURE test_proc_sk(
p_class_array IN p_type,
p_emp_record OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN p_emp_record FOR
SELECT *
FROM dept
WHERE deptno = 10
AND ( p_class_array IS EMPTY OR dname MEMBER OF p_class_array );
END;
You will have to bind the parameter p_class_array .In dynamic SQL you would prefix them with a colon (:). If you use EXECUTE IMMEDIATE or OPEN ... FOR, you will bind your parameters via position to avoid sql injection.
Also note that in your Select statement you are doing *, so while execution you must declare individual variable to hold the outcome. To aviod mistake you must declare the column name in the select statement as shown below:
CREATE OR REPLACE PROCEDURE test_proc_sk(
p_class_array IN p_type,
p_emp_record OUT SYS_REFCURSOR)
IS
lv_stmt VARCHAR2(200);
BEGIN
lv_stmt := 'Select deptno,dname from dept where deptno = 10 ';
IF(p_class_array.count > 0) THEN
lv_stmt := lv_stmt || ' AND dname IN (select column_value from table(:p_class_array))';
END IF;
dbms_output.put_line(lv_stmt);
OPEN p_emp_record FOR lv_stmt using p_class_array;
END;
/
Execution:
SQL> Select * from dept ;
SQL> /
DEPTNO DNAME
---------- ----------------------------------------------------------------------------------------------------
10 CTS
20 WIPRO
30 TCS
SQL> DECLARE
vr p_type:= p_type();
x SYS_REFCURSOR;
z VARCHAR2(10);
z1 number;
BEGIN
vr.extend(2);
vr (1) := 'CTS';
vr (2) := 'TCS';
test_proc_sk (vr, x);
LOOP
FETCH x INTO z1,z;
EXIT WHEN x%NOTFOUND;
DBMS_OUTPUT.PUT_LINE (z ||' ' ||z1);
END LOOP;
END;
/
SQL> /
Select deptno,dname from dept where deptno = 10 AND dname IN (select column_value from table(:p_class_array))
CTS 10
PL/SQL procedure successfully completed.

Oracle: Return multiple values in a function

I'm trying to return a multiple values in a %rowtype from a function using two table(employees and departments), but it not working for me.
create or replace function get_employee
(loc in number)
return mv_emp%rowtype
as
emp_record mv_emp%rowtype;
begin
select a.first_name, a.last_name, b.department_name into emp_record
from employees a, departments b
where a.department_id=b.department_id and location_id=loc;
return(emp_record);
end;
The above function compiled without any error? What is the type of MV_EMP? Ideally, it should be something like below.
create or replace type emp_type
(
first_name varchar2(20)
, last_name varchar2(20)
, depart_name varchar2(20)
)
/
create or replace function get_employee
(loc in number)
return emp_type
as
emp_record emp_type;
begin
select a.first_name, a.last_name, b.department_name into emp_record
from employees a, departments b
where a.department_id=b.department_id and location_id=loc;
return(emp_record);
end;
create type t_row as object (a varchar2(10));
create type t_row_tab as table of t_row;
We will now create a function which will split the input string.
create or replace function get_number(pv_no_list in varchar2) return t_row_tab is
lv_no_list t_row_tab := t_row_tab();
begin
for i in (SELECT distinct REGEXP_SUBSTR(pv_no_list, '[^,]+', 1, LEVEL) no_list FROM dual
CONNECT BY REGEXP_SUBSTR(pv_no_list, '[^,]+', 1, LEVEL) IS NOT NULL)
loop
lv_no_list.extend;
lv_no_list(lv_no_list.last) := t_row(i.no_list);
end loop;
return lv_no_list;
end get_number;
Once the function is in place we can use the table clause of sql statement to get the desired result. As desired we got multiple values returned from the function.
SQL> select * from table(get_number('1,2,3,4'));
A
----------
1
3
2
4
So now our function is simply behaving like a table. There can be a situation where you want these comma separated values to be a part of "IN" clause.
For example :
select * from dummy_table where dummy_column in ('1,2,3,4');
But the above query will not work as '1,2,3,4' is a string and not individual numbers. To solve this problem you can simply use following query.
select * from dummy_table where dummy_column in ( select * from table(get_number('1,2,3,4')) );
References : http://www.oraclebin.com/2012/12/returning-multiple-values-from-function.html
CREATE OR replace FUNCTION Funmultiple(deptno_in IN NUMBER)
RETURN NUMBER AS v_refcursur SYS_REFCURSOR;
BEGIN
OPEN v_refcursor FOR
SELECT *
FROM emp
WHERE deptno = deptno_in;
retun v_refcursor;
END;
To call it, use:
variable x number
exec :x := FunMultiple(10);
print x